From a67dbb391eef39d7f3279c462eb318b02733daa8 Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Thu, 20 Feb 2025 18:00:57 +0100 Subject: [PATCH 1/2] 126860: POC for optimising server health checking --- src/app/core/core-state.model.ts | 2 + src/app/core/core.reducers.ts | 2 + src/app/core/data/request.effects.ts | 12 ++- src/app/core/data/root-data.service.ts | 17 +--- src/app/core/history/server-health.actions.ts | 24 ++++++ src/app/core/history/server-health.reducer.ts | 27 ++++++ .../core/server-check/server-check.guard.ts | 28 +++---- .../core/server-check/server-check.service.ts | 83 +++++++++++++++++++ src/app/root/root.component.html | 4 +- src/app/root/root.component.ts | 19 +++-- 10 files changed, 175 insertions(+), 43 deletions(-) create mode 100644 src/app/core/history/server-health.actions.ts create mode 100644 src/app/core/history/server-health.reducer.ts create mode 100644 src/app/core/server-check/server-check.service.ts diff --git a/src/app/core/core-state.model.ts b/src/app/core/core-state.model.ts index b8211fdb555..22d9a538fe2 100644 --- a/src/app/core/core-state.model.ts +++ b/src/app/core/core-state.model.ts @@ -11,6 +11,7 @@ import { JsonPatchOperationsState } from './json-patch/json-patch-operations.red import { MetaTagState } from './metadata/meta-tag.reducer'; import { RouteState } from './services/route.reducer'; import { RequestState } from './data/request-state.model'; +import { ServerHealthState } from './history/server-health.reducer'; /** * The core sub-state in the NgRx store @@ -22,6 +23,7 @@ export interface CoreState { 'cache/object-updates': ObjectUpdatesState; 'data/request': RequestState; 'history': HistoryState; + 'serverHealth': ServerHealthState; 'index': MetaIndexState; 'auth': AuthState; 'json/patch': JsonPatchOperationsState; diff --git a/src/app/core/core.reducers.ts b/src/app/core/core.reducers.ts index c0165c53848..1730ea1800c 100644 --- a/src/app/core/core.reducers.ts +++ b/src/app/core/core.reducers.ts @@ -14,6 +14,7 @@ import { import { historyReducer } from './history/history.reducer'; import { metaTagReducer } from './metadata/meta-tag.reducer'; import { CoreState } from './core-state.model'; +import { serverHealthReducer } from './history/server-health.reducer'; export const coreReducers: ActionReducerMap = { 'bitstreamFormats': bitstreamFormatReducer, @@ -22,6 +23,7 @@ export const coreReducers: ActionReducerMap = { 'cache/object-updates': objectUpdatesReducer, 'data/request': requestReducer, 'history': historyReducer, + 'serverHealth': serverHealthReducer, 'index': indexReducer, 'auth': authReducer, 'json/patch': jsonPatchOperationsReducer, diff --git a/src/app/core/data/request.effects.ts b/src/app/core/data/request.effects.ts index 889d909bfa3..2d364f27e1b 100644 --- a/src/app/core/data/request.effects.ts +++ b/src/app/core/data/request.effects.ts @@ -21,11 +21,12 @@ import { ParsedResponse } from '../cache/response.models'; import { RequestError } from './request-error.model'; import { RestRequestWithResponseParser } from './rest-request-with-response-parser.model'; import { RequestEntry } from './request-entry.model'; +import { ServerCheckService } from '../server-check/server-check.service'; @Injectable() export class RequestEffects { - execute = createEffect(() => this.actions$.pipe( + execute = createEffect(() => this.actions$.pipe( ofType(RequestActionTypes.EXECUTE), mergeMap((action: RequestExecuteAction) => { return this.requestService.getByUUID(action.payload).pipe( @@ -46,6 +47,7 @@ export class RequestEffects { catchError((error: RequestError) => { if (hasValue(error.statusCode)) { // if it's an error returned by the server, complete the request + this.serverCheckService.checkAndUpdateServerAvailability(request, error); return [new RequestErrorAction(request.uuid, error.statusCode, error.message)]; } else { // if it's a client side error, throw it @@ -64,7 +66,7 @@ export class RequestEffects { * This assumes that the server cached everything a negligible * time ago, and will likely need to be revisited later */ - fixTimestampsOnRehydrate = createEffect(() => this.actions$ + fixTimestampsOnRehydrate = createEffect(() => this.actions$ .pipe(ofType(StoreActionTypes.REHYDRATE), map(() => new ResetResponseTimestampsAction(new Date().getTime())) )); @@ -73,7 +75,9 @@ export class RequestEffects { private actions$: Actions, private restApi: DspaceRestService, private injector: Injector, - protected requestService: RequestService - ) { } + protected requestService: RequestService, + protected serverCheckService: ServerCheckService, + ) { + } } diff --git a/src/app/core/data/root-data.service.ts b/src/app/core/data/root-data.service.ts index 54fe614d3e8..389f81a0a6c 100644 --- a/src/app/core/data/root-data.service.ts +++ b/src/app/core/data/root-data.service.ts @@ -8,11 +8,11 @@ import { Observable, of as observableOf } from 'rxjs'; import { RemoteData } from './remote-data'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { DspaceRestService } from '../dspace-rest/dspace-rest.service'; -import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; -import { catchError, map } from 'rxjs/operators'; +import { catchError, map, tap } from 'rxjs/operators'; import { BaseDataService } from './base/base-data.service'; import { ObjectCacheService } from '../cache/object-cache.service'; import { dataService } from './base/data-service.decorator'; +import { getFirstCompletedRemoteData } from '../shared/operators'; /** * A service to retrieve the {@link Root} object from the REST API. @@ -30,19 +30,6 @@ export class RootDataService extends BaseDataService { super('', requestService, rdbService, objectCache, halService, 6 * 60 * 60 * 1000); } - /** - * Check if root endpoint is available - */ - checkServerAvailability(): Observable { - return this.restService.get(this.halService.getRootHref()).pipe( - catchError((err ) => { - console.error(err); - return observableOf(false); - }), - map((res: RawRestResponse) => res.statusCode === 200) - ); - } - /** * Find the {@link Root} object of the REST API * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's diff --git a/src/app/core/history/server-health.actions.ts b/src/app/core/history/server-health.actions.ts new file mode 100644 index 00000000000..78c0d900de5 --- /dev/null +++ b/src/app/core/history/server-health.actions.ts @@ -0,0 +1,24 @@ +/* eslint-disable max-classes-per-file */ +import { Action } from '@ngrx/store'; + +import { type } from '../../shared/ngrx/type'; + +export const ServerHealthActionTypes = { + UPDATE_SERVER_HEALTH: type('dspace/server-health/UPDATE_SERVER_HEALTH'), +}; + + +export class UpdateServerHealthAction implements Action { + type = ServerHealthActionTypes.UPDATE_SERVER_HEALTH; + payload: { + isOnline: boolean; + }; + + constructor(isOnline: boolean) { + this.payload = {isOnline}; + } +} + + +export type ServerHealthAction + = UpdateServerHealthAction; diff --git a/src/app/core/history/server-health.reducer.ts b/src/app/core/history/server-health.reducer.ts new file mode 100644 index 00000000000..0df18afcd7a --- /dev/null +++ b/src/app/core/history/server-health.reducer.ts @@ -0,0 +1,27 @@ +import { ServerHealthAction, ServerHealthActionTypes, UpdateServerHealthAction } from './server-health.actions'; + +/** + * The auth state. + */ +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface ServerHealthState { + isOnline: boolean; +} + +/** + * The initial state. + */ +const initialState: ServerHealthState = {isOnline: true}; + +export function serverHealthReducer(state = initialState, action: ServerHealthAction): ServerHealthState { + switch (action.type) { + + case ServerHealthActionTypes.UPDATE_SERVER_HEALTH: { + return {isOnline: (action as UpdateServerHealthAction).payload.isOnline}; + } + + default: { + return state; + } + } +} diff --git a/src/app/core/server-check/server-check.guard.ts b/src/app/core/server-check/server-check.guard.ts index 8a0e26c01da..ae3426043d6 100644 --- a/src/app/core/server-check/server-check.guard.ts +++ b/src/app/core/server-check/server-check.guard.ts @@ -1,11 +1,10 @@ import { Injectable } from '@angular/core'; -import { ActivatedRouteSnapshot, CanActivateChild, Router, RouterStateSnapshot } from '@angular/router'; +import { ActivatedRouteSnapshot, CanActivateChild, RouterStateSnapshot } from '@angular/router'; import { Observable } from 'rxjs'; -import { take, tap } from 'rxjs/operators'; +import { tap } from 'rxjs/operators'; +import { ServerCheckService } from './server-check.service'; -import { RootDataService } from '../data/root-data.service'; -import { getPageInternalServerErrorRoute } from '../../app-routing-paths'; @Injectable({ providedIn: 'root' @@ -15,7 +14,7 @@ import { getPageInternalServerErrorRoute } from '../../app-routing-paths'; * If not redirect to 500 error page */ export class ServerCheckGuard implements CanActivateChild { - constructor(private router: Router, private rootDataService: RootDataService) { + constructor(protected serverCheckService: ServerCheckService,) { } /** @@ -24,16 +23,13 @@ export class ServerCheckGuard implements CanActivateChild { canActivateChild( route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - - return this.rootDataService.checkServerAvailability().pipe( - take(1), - tap((isAvailable: boolean) => { - if (!isAvailable) { - this.rootDataService.invalidateRootCache(); - this.router.navigateByUrl(getPageInternalServerErrorRoute()); - } - }) - ); - + return this.serverCheckService.checkServerAvailabilityFromStore() + .pipe( + tap((isReallyAvailable: boolean) => { + if (!isReallyAvailable) { + this.serverCheckService.invalidateCacheAndNavigateToInternalServerErrorPage(); + } + }) + ); } } diff --git a/src/app/core/server-check/server-check.service.ts b/src/app/core/server-check/server-check.service.ts new file mode 100644 index 00000000000..3d2d1f23f65 --- /dev/null +++ b/src/app/core/server-check/server-check.service.ts @@ -0,0 +1,83 @@ +import { Injectable } from '@angular/core'; +import { Observable, of, of as observableOf, switchMap } from 'rxjs'; +import { getFirstCompletedRemoteData } from '../shared/operators'; +import { catchError, map } from 'rxjs/operators'; +import { RemoteData } from '../data/remote-data'; +import { Root } from '../data/root.model'; +import { RootDataService } from '../data/root-data.service'; +import { AppState } from '../../app.reducer'; +import { createSelector, Store } from '@ngrx/store'; +import { UpdateServerHealthAction } from '../history/server-health.actions'; +import { getPageInternalServerErrorRoute } from '../../app-routing-paths'; +import { Router } from '@angular/router'; +import { coreSelector } from '../core.selectors'; +import { CoreState } from '../core-state.model'; +import { RestRequestWithResponseParser } from '../data/rest-request-with-response-parser.model'; +import { RequestError } from '../data/request-error.model'; +import { RESTURLCombiner } from '../url-combiner/rest-url-combiner'; + +export const getServerhealthState = createSelector(coreSelector, (state: CoreState) => state.serverHealth); + + +@Injectable({ + providedIn: 'root' +}) + +export class ServerCheckService { + + constructor( + protected rootDataService: RootDataService, + protected store: Store, + protected router: Router, + ) { + } + + + /** + * Check if root endpoint is available + */ + checkServerAvailabilityFromStore(): Observable { + return this.store.select(getServerhealthState).pipe( + map((state) => state.isOnline), + switchMap((isAvailable: boolean) => { + if (!isAvailable) { + this.rootDataService.invalidateRootCache(); + return this.isRootServerAvailable(); + } else { + return of(isAvailable); + } + }) + ); + } + + + private isRootServerAvailable() { + return this.rootDataService.findRoot(false).pipe( + getFirstCompletedRemoteData(), + catchError((err) => { + console.error(err); + return observableOf(false); + }), + map((rd: RemoteData) => rd.statusCode === 200) + ); + } + + checkAndUpdateServerAvailability(request: RestRequestWithResponseParser, error: RequestError) { + if ((error.statusCode === 500 || error.statusCode === 0) && request.method === 'GET' && this.router.url != getPageInternalServerErrorRoute() && request.href !== new RESTURLCombiner().toString()) { + this.isRootServerAvailable().pipe( + ).subscribe((isAvailable: boolean) => { + if (!isAvailable) { + this.store.dispatch(new UpdateServerHealthAction(false)); + this.invalidateCacheAndNavigateToInternalServerErrorPage(); + } + }); + } + } + + invalidateCacheAndNavigateToInternalServerErrorPage() { + this.rootDataService.invalidateRootCache(); + this.router.navigateByUrl(getPageInternalServerErrorRoute()); + } + + +} diff --git a/src/app/root/root.component.html b/src/app/root/root.component.html index bf49e507c0b..94a5792a6bf 100644 --- a/src/app/root/root.component.html +++ b/src/app/root/root.component.html @@ -9,10 +9,10 @@
-
+
-
+
diff --git a/src/app/root/root.component.ts b/src/app/root/root.component.ts index 3c2d65fc1f5..9e43d0bde65 100644 --- a/src/app/root/root.component.ts +++ b/src/app/root/root.component.ts @@ -1,6 +1,6 @@ -import { map, startWith } from 'rxjs/operators'; +import { map, startWith, take, tap } from 'rxjs/operators'; import { Component, Inject, Input, OnInit } from '@angular/core'; -import { Router } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { combineLatest as combineLatestObservable, Observable, of } from 'rxjs'; import { Store } from '@ngrx/store'; @@ -20,6 +20,7 @@ import { slideSidebarPadding } from '../shared/animations/slide'; import { MenuID } from '../shared/menu/menu-id.model'; import { getPageInternalServerErrorRoute } from '../app-routing-paths'; import { hasValueOperator } from '../shared/empty.util'; +import { RootDataService } from '../core/data/root-data.service'; @Component({ selector: 'ds-root', @@ -46,6 +47,8 @@ export class RootComponent implements OnInit { */ @Input() shouldShowRouteLoader: boolean; + shouldShowRouteLoader$: Observable; + constructor( @Inject(NativeWindowService) private _window: NativeWindowRef, private translate: TranslateService, @@ -56,12 +59,15 @@ export class RootComponent implements OnInit { private router: Router, private cssService: CSSVariableService, private menuService: MenuService, - private windowService: HostWindowService + private windowService: HostWindowService, + private rootDataService: RootDataService, + private activateRoute: ActivatedRoute, ) { this.notificationOptions = environment.notifications; } ngOnInit() { + this.sidebarVisible = this.menuService.isMenuVisibleWithVisibleSections(MenuID.ADMIN); this.collapsedSidebarWidth = this.cssService.getVariable('--ds-collapsed-sidebar-width').pipe(hasValueOperator()); @@ -74,8 +80,9 @@ export class RootComponent implements OnInit { startWith(true), ); - if (this.router.url === getPageInternalServerErrorRoute()) { - this.shouldShowRouteLoader = false; - } + this.shouldShowRouteLoader$ = this.activateRoute.url.pipe( + map((url) => url.join('/')), + map((url) => url === getPageInternalServerErrorRoute()), + ) } } From cc76eb21485d973817db6d4fdb937e78192ee77b Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Thu, 6 Mar 2025 17:15:05 +0100 Subject: [PATCH 2/2] 126860: Fix first load issue, rename server check/health to status and fix root component internal page check --- src/app/app-routing.module.ts | 4 +- src/app/core/core-state.model.ts | 4 +- src/app/core/core.reducers.ts | 4 +- src/app/core/data/request.effects.ts | 14 +- src/app/core/data/root-data.service.spec.ts | 46 ++---- src/app/core/data/root-data.service.ts | 6 +- src/app/core/history/server-health.actions.ts | 24 --- src/app/core/history/server-health.reducer.ts | 27 ---- src/app/core/history/server-status.actions.ts | 24 +++ src/app/core/history/server-status.reducer.ts | 25 +++ .../server-check/server-check.guard.spec.ts | 68 -------- .../core/server-check/server-check.guard.ts | 35 ----- .../core/server-check/server-check.service.ts | 83 ---------- .../server-status-guard.service.ts | 35 +++++ .../server-check/server-status.guard.spec.ts | 59 +++++++ .../server-status.service.spec.ts | 148 ++++++++++++++++++ .../server-check/server-status.service.ts | 100 ++++++++++++ src/app/root/root.component.html | 2 +- src/app/root/root.component.ts | 56 ++++--- 19 files changed, 452 insertions(+), 312 deletions(-) delete mode 100644 src/app/core/history/server-health.actions.ts delete mode 100644 src/app/core/history/server-health.reducer.ts create mode 100644 src/app/core/history/server-status.actions.ts create mode 100644 src/app/core/history/server-status.reducer.ts delete mode 100644 src/app/core/server-check/server-check.guard.spec.ts delete mode 100644 src/app/core/server-check/server-check.guard.ts delete mode 100644 src/app/core/server-check/server-check.service.ts create mode 100644 src/app/core/server-check/server-status-guard.service.ts create mode 100644 src/app/core/server-check/server-status.guard.spec.ts create mode 100644 src/app/core/server-check/server-status.service.spec.ts create mode 100644 src/app/core/server-check/server-status.service.ts diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index deb68f1ea92..1d368e67961 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -37,7 +37,7 @@ import { import { ThemedPageInternalServerErrorComponent } from './page-internal-server-error/themed-page-internal-server-error.component'; -import { ServerCheckGuard } from './core/server-check/server-check.guard'; +import { ServerStatusGuard } from './core/server-check/server-status-guard.service'; import { MenuResolver } from './menu.resolver'; import { ThemedPageErrorComponent } from './page-error/themed-page-error.component'; @@ -49,7 +49,7 @@ import { ThemedPageErrorComponent } from './page-error/themed-page-error.compone { path: '', canActivate: [AuthBlockingGuard], - canActivateChild: [ServerCheckGuard], + canActivateChild: [ServerStatusGuard], resolve: [MenuResolver], children: [ { path: '', redirectTo: '/home', pathMatch: 'full' }, diff --git a/src/app/core/core-state.model.ts b/src/app/core/core-state.model.ts index 22d9a538fe2..8bb8e766286 100644 --- a/src/app/core/core-state.model.ts +++ b/src/app/core/core-state.model.ts @@ -11,7 +11,7 @@ import { JsonPatchOperationsState } from './json-patch/json-patch-operations.red import { MetaTagState } from './metadata/meta-tag.reducer'; import { RouteState } from './services/route.reducer'; import { RequestState } from './data/request-state.model'; -import { ServerHealthState } from './history/server-health.reducer'; +import { ServerStatusState } from './history/server-status.reducer'; /** * The core sub-state in the NgRx store @@ -23,7 +23,7 @@ export interface CoreState { 'cache/object-updates': ObjectUpdatesState; 'data/request': RequestState; 'history': HistoryState; - 'serverHealth': ServerHealthState; + 'serverStatus': ServerStatusState; 'index': MetaIndexState; 'auth': AuthState; 'json/patch': JsonPatchOperationsState; diff --git a/src/app/core/core.reducers.ts b/src/app/core/core.reducers.ts index 1730ea1800c..c165504be2d 100644 --- a/src/app/core/core.reducers.ts +++ b/src/app/core/core.reducers.ts @@ -14,7 +14,7 @@ import { import { historyReducer } from './history/history.reducer'; import { metaTagReducer } from './metadata/meta-tag.reducer'; import { CoreState } from './core-state.model'; -import { serverHealthReducer } from './history/server-health.reducer'; +import { serverStatusReducer } from './history/server-status.reducer'; export const coreReducers: ActionReducerMap = { 'bitstreamFormats': bitstreamFormatReducer, @@ -23,7 +23,7 @@ export const coreReducers: ActionReducerMap = { 'cache/object-updates': objectUpdatesReducer, 'data/request': requestReducer, 'history': historyReducer, - 'serverHealth': serverHealthReducer, + 'serverStatus': serverStatusReducer, 'index': indexReducer, 'auth': authReducer, 'json/patch': jsonPatchOperationsReducer, diff --git a/src/app/core/data/request.effects.ts b/src/app/core/data/request.effects.ts index 2d364f27e1b..af58a930a2a 100644 --- a/src/app/core/data/request.effects.ts +++ b/src/app/core/data/request.effects.ts @@ -21,7 +21,7 @@ import { ParsedResponse } from '../cache/response.models'; import { RequestError } from './request-error.model'; import { RestRequestWithResponseParser } from './rest-request-with-response-parser.model'; import { RequestEntry } from './request-entry.model'; -import { ServerCheckService } from '../server-check/server-check.service'; +import { ServerStatusService } from '../server-check/server-status.service'; @Injectable() export class RequestEffects { @@ -46,8 +46,14 @@ export class RequestEffects { map((response: ParsedResponse) => new RequestSuccessAction(request.uuid, response.statusCode, response.link, response.unCacheableObject)), catchError((error: RequestError) => { if (hasValue(error.statusCode)) { - // if it's an error returned by the server, complete the request - this.serverCheckService.checkAndUpdateServerAvailability(request, error); + // if it's an error returned by the server, check if the server is still running and update its status + // then navigate to the internal error page while still completing the request + this.serverStatusService.checkAndUpdateServerStatus(request, error) + .subscribe((isAvailable: boolean) => { + if (!isAvailable) { + this.serverStatusService.navigateToInternalServerErrorPage(); + } + }); return [new RequestErrorAction(request.uuid, error.statusCode, error.message)]; } else { // if it's a client side error, throw it @@ -76,7 +82,7 @@ export class RequestEffects { private restApi: DspaceRestService, private injector: Injector, protected requestService: RequestService, - protected serverCheckService: ServerCheckService, + protected serverStatusService: ServerStatusService, ) { } diff --git a/src/app/core/data/root-data.service.spec.ts b/src/app/core/data/root-data.service.spec.ts index b65449d0075..fdec57a1870 100644 --- a/src/app/core/data/root-data.service.spec.ts +++ b/src/app/core/data/root-data.service.spec.ts @@ -1,16 +1,14 @@ import { RootDataService } from './root-data.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; -import { Observable, of } from 'rxjs'; +import { Observable } from 'rxjs'; import { RemoteData } from './remote-data'; import { Root } from './root.model'; -import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; -import { cold } from 'jasmine-marbles'; describe('RootDataService', () => { let service: RootDataService; let halService: HALEndpointService; - let restService; + let requestService; let rootEndpoint; let findByHrefSpy; @@ -19,10 +17,10 @@ describe('RootDataService', () => { halService = jasmine.createSpyObj('halService', { getRootHref: rootEndpoint, }); - restService = jasmine.createSpyObj('halService', { - get: jasmine.createSpy('get'), + requestService = jasmine.createSpyObj('halService', { + setStaleByHrefSubstring: {}, }); - service = new RootDataService(null, null, null, halService, restService); + service = new RootDataService(requestService, null, null, halService); findByHrefSpy = spyOn(service as any, 'findByHref'); findByHrefSpy.and.returnValue(createSuccessfulRemoteDataObject$({})); @@ -43,36 +41,14 @@ describe('RootDataService', () => { }); }); - describe('checkServerAvailability', () => { - let result$: Observable; + describe('invalidateRootCache', () => { + it('should call setStaleByHrefSubstring with the root endpoint href', () => { + service.invalidateRootCache(); - it('should return observable of true when root endpoint is available', () => { - const mockResponse = { - statusCode: 200, - statusText: 'OK' - } as RawRestResponse; - - restService.get.and.returnValue(of(mockResponse)); - result$ = service.checkServerAvailability(); - - expect(result$).toBeObservable(cold('(a|)', { - a: true - })); + expect(halService.getRootHref).toHaveBeenCalled(); + expect(requestService.setStaleByHrefSubstring).toHaveBeenCalledWith(rootEndpoint); }); + }); - it('should return observable of false when root endpoint is not available', () => { - const mockResponse = { - statusCode: 500, - statusText: 'Internal Server Error' - } as RawRestResponse; - - restService.get.and.returnValue(of(mockResponse)); - result$ = service.checkServerAvailability(); - - expect(result$).toBeObservable(cold('(a|)', { - a: false - })); - }); - }); }); diff --git a/src/app/core/data/root-data.service.ts b/src/app/core/data/root-data.service.ts index 389f81a0a6c..daff0cbf106 100644 --- a/src/app/core/data/root-data.service.ts +++ b/src/app/core/data/root-data.service.ts @@ -4,15 +4,12 @@ import { ROOT } from './root.resource-type'; import { RequestService } from './request.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { Observable, of as observableOf } from 'rxjs'; +import { Observable } from 'rxjs'; import { RemoteData } from './remote-data'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; -import { DspaceRestService } from '../dspace-rest/dspace-rest.service'; -import { catchError, map, tap } from 'rxjs/operators'; import { BaseDataService } from './base/base-data.service'; import { ObjectCacheService } from '../cache/object-cache.service'; import { dataService } from './base/data-service.decorator'; -import { getFirstCompletedRemoteData } from '../shared/operators'; /** * A service to retrieve the {@link Root} object from the REST API. @@ -25,7 +22,6 @@ export class RootDataService extends BaseDataService { protected rdbService: RemoteDataBuildService, protected objectCache: ObjectCacheService, protected halService: HALEndpointService, - protected restService: DspaceRestService, ) { super('', requestService, rdbService, objectCache, halService, 6 * 60 * 60 * 1000); } diff --git a/src/app/core/history/server-health.actions.ts b/src/app/core/history/server-health.actions.ts deleted file mode 100644 index 78c0d900de5..00000000000 --- a/src/app/core/history/server-health.actions.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* eslint-disable max-classes-per-file */ -import { Action } from '@ngrx/store'; - -import { type } from '../../shared/ngrx/type'; - -export const ServerHealthActionTypes = { - UPDATE_SERVER_HEALTH: type('dspace/server-health/UPDATE_SERVER_HEALTH'), -}; - - -export class UpdateServerHealthAction implements Action { - type = ServerHealthActionTypes.UPDATE_SERVER_HEALTH; - payload: { - isOnline: boolean; - }; - - constructor(isOnline: boolean) { - this.payload = {isOnline}; - } -} - - -export type ServerHealthAction - = UpdateServerHealthAction; diff --git a/src/app/core/history/server-health.reducer.ts b/src/app/core/history/server-health.reducer.ts deleted file mode 100644 index 0df18afcd7a..00000000000 --- a/src/app/core/history/server-health.reducer.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { ServerHealthAction, ServerHealthActionTypes, UpdateServerHealthAction } from './server-health.actions'; - -/** - * The auth state. - */ -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface ServerHealthState { - isOnline: boolean; -} - -/** - * The initial state. - */ -const initialState: ServerHealthState = {isOnline: true}; - -export function serverHealthReducer(state = initialState, action: ServerHealthAction): ServerHealthState { - switch (action.type) { - - case ServerHealthActionTypes.UPDATE_SERVER_HEALTH: { - return {isOnline: (action as UpdateServerHealthAction).payload.isOnline}; - } - - default: { - return state; - } - } -} diff --git a/src/app/core/history/server-status.actions.ts b/src/app/core/history/server-status.actions.ts new file mode 100644 index 00000000000..efc01ac16d5 --- /dev/null +++ b/src/app/core/history/server-status.actions.ts @@ -0,0 +1,24 @@ +/* eslint-disable max-classes-per-file */ +import { Action } from '@ngrx/store'; + +import { type } from '../../shared/ngrx/type'; + +export const ServerStatusActionTypes = { + UPDATE_SERVER_STATUS: type('dspace/server-status/UPDATE_SERVER_STATUS'), +}; + + +export class UpdateServerStatusAction implements Action { + type = ServerStatusActionTypes.UPDATE_SERVER_STATUS; + payload: { + isAvailable: boolean; + }; + + constructor(isAvailable: boolean) { + this.payload = {isAvailable}; + } +} + + +export type ServerStatusAction + = UpdateServerStatusAction; diff --git a/src/app/core/history/server-status.reducer.ts b/src/app/core/history/server-status.reducer.ts new file mode 100644 index 00000000000..a0f4f98cab1 --- /dev/null +++ b/src/app/core/history/server-status.reducer.ts @@ -0,0 +1,25 @@ +import { ServerStatusAction, ServerStatusActionTypes, UpdateServerStatusAction } from './server-status.actions'; + +/** + * The auth state. + */ +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface ServerStatusState { + isAvailable: boolean; +} + +/** + * The initial state. + */ +const initialState: ServerStatusState = {isAvailable: true}; + +export function serverStatusReducer(state = initialState, action: ServerStatusAction): ServerStatusState { + switch (action.type) { + case ServerStatusActionTypes.UPDATE_SERVER_STATUS: { + return {isAvailable: (action as UpdateServerStatusAction).payload.isAvailable}; + } + default: { + return state; + } + } +} diff --git a/src/app/core/server-check/server-check.guard.spec.ts b/src/app/core/server-check/server-check.guard.spec.ts deleted file mode 100644 index 1f126be5e55..00000000000 --- a/src/app/core/server-check/server-check.guard.spec.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { ServerCheckGuard } from './server-check.guard'; -import { Router } from '@angular/router'; - -import { of } from 'rxjs'; -import { take } from 'rxjs/operators'; - -import { getPageInternalServerErrorRoute } from '../../app-routing-paths'; -import { RootDataService } from '../data/root-data.service'; -import SpyObj = jasmine.SpyObj; - -describe('ServerCheckGuard', () => { - let guard: ServerCheckGuard; - let router: SpyObj; - let rootDataServiceStub: SpyObj; - - rootDataServiceStub = jasmine.createSpyObj('RootDataService', { - checkServerAvailability: jasmine.createSpy('checkServerAvailability'), - invalidateRootCache: jasmine.createSpy('invalidateRootCache') - }); - router = jasmine.createSpyObj('Router', { - navigateByUrl: jasmine.createSpy('navigateByUrl') - }); - - beforeEach(() => { - guard = new ServerCheckGuard(router, rootDataServiceStub); - }); - - afterEach(() => { - router.navigateByUrl.calls.reset(); - rootDataServiceStub.invalidateRootCache.calls.reset(); - }); - - it('should be created', () => { - expect(guard).toBeTruthy(); - }); - - describe('when root endpoint has succeeded', () => { - beforeEach(() => { - rootDataServiceStub.checkServerAvailability.and.returnValue(of(true)); - }); - - it('should not redirect to error page', () => { - guard.canActivateChild({} as any, {} as any).pipe( - take(1) - ).subscribe((canActivate: boolean) => { - expect(canActivate).toEqual(true); - expect(rootDataServiceStub.invalidateRootCache).not.toHaveBeenCalled(); - expect(router.navigateByUrl).not.toHaveBeenCalled(); - }); - }); - }); - - describe('when root endpoint has not succeeded', () => { - beforeEach(() => { - rootDataServiceStub.checkServerAvailability.and.returnValue(of(false)); - }); - - it('should redirect to error page', () => { - guard.canActivateChild({} as any, {} as any).pipe( - take(1) - ).subscribe((canActivate: boolean) => { - expect(canActivate).toEqual(false); - expect(rootDataServiceStub.invalidateRootCache).toHaveBeenCalled(); - expect(router.navigateByUrl).toHaveBeenCalledWith(getPageInternalServerErrorRoute()); - }); - }); - }); -}); diff --git a/src/app/core/server-check/server-check.guard.ts b/src/app/core/server-check/server-check.guard.ts deleted file mode 100644 index ae3426043d6..00000000000 --- a/src/app/core/server-check/server-check.guard.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Injectable } from '@angular/core'; -import { ActivatedRouteSnapshot, CanActivateChild, RouterStateSnapshot } from '@angular/router'; - -import { Observable } from 'rxjs'; -import { tap } from 'rxjs/operators'; -import { ServerCheckService } from './server-check.service'; - - -@Injectable({ - providedIn: 'root' -}) -/** - * A guard that checks if root api endpoint is reachable. - * If not redirect to 500 error page - */ -export class ServerCheckGuard implements CanActivateChild { - constructor(protected serverCheckService: ServerCheckService,) { - } - - /** - * True when root api endpoint is reachable. - */ - canActivateChild( - route: ActivatedRouteSnapshot, - state: RouterStateSnapshot): Observable { - return this.serverCheckService.checkServerAvailabilityFromStore() - .pipe( - tap((isReallyAvailable: boolean) => { - if (!isReallyAvailable) { - this.serverCheckService.invalidateCacheAndNavigateToInternalServerErrorPage(); - } - }) - ); - } -} diff --git a/src/app/core/server-check/server-check.service.ts b/src/app/core/server-check/server-check.service.ts deleted file mode 100644 index 3d2d1f23f65..00000000000 --- a/src/app/core/server-check/server-check.service.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { Injectable } from '@angular/core'; -import { Observable, of, of as observableOf, switchMap } from 'rxjs'; -import { getFirstCompletedRemoteData } from '../shared/operators'; -import { catchError, map } from 'rxjs/operators'; -import { RemoteData } from '../data/remote-data'; -import { Root } from '../data/root.model'; -import { RootDataService } from '../data/root-data.service'; -import { AppState } from '../../app.reducer'; -import { createSelector, Store } from '@ngrx/store'; -import { UpdateServerHealthAction } from '../history/server-health.actions'; -import { getPageInternalServerErrorRoute } from '../../app-routing-paths'; -import { Router } from '@angular/router'; -import { coreSelector } from '../core.selectors'; -import { CoreState } from '../core-state.model'; -import { RestRequestWithResponseParser } from '../data/rest-request-with-response-parser.model'; -import { RequestError } from '../data/request-error.model'; -import { RESTURLCombiner } from '../url-combiner/rest-url-combiner'; - -export const getServerhealthState = createSelector(coreSelector, (state: CoreState) => state.serverHealth); - - -@Injectable({ - providedIn: 'root' -}) - -export class ServerCheckService { - - constructor( - protected rootDataService: RootDataService, - protected store: Store, - protected router: Router, - ) { - } - - - /** - * Check if root endpoint is available - */ - checkServerAvailabilityFromStore(): Observable { - return this.store.select(getServerhealthState).pipe( - map((state) => state.isOnline), - switchMap((isAvailable: boolean) => { - if (!isAvailable) { - this.rootDataService.invalidateRootCache(); - return this.isRootServerAvailable(); - } else { - return of(isAvailable); - } - }) - ); - } - - - private isRootServerAvailable() { - return this.rootDataService.findRoot(false).pipe( - getFirstCompletedRemoteData(), - catchError((err) => { - console.error(err); - return observableOf(false); - }), - map((rd: RemoteData) => rd.statusCode === 200) - ); - } - - checkAndUpdateServerAvailability(request: RestRequestWithResponseParser, error: RequestError) { - if ((error.statusCode === 500 || error.statusCode === 0) && request.method === 'GET' && this.router.url != getPageInternalServerErrorRoute() && request.href !== new RESTURLCombiner().toString()) { - this.isRootServerAvailable().pipe( - ).subscribe((isAvailable: boolean) => { - if (!isAvailable) { - this.store.dispatch(new UpdateServerHealthAction(false)); - this.invalidateCacheAndNavigateToInternalServerErrorPage(); - } - }); - } - } - - invalidateCacheAndNavigateToInternalServerErrorPage() { - this.rootDataService.invalidateRootCache(); - this.router.navigateByUrl(getPageInternalServerErrorRoute()); - } - - -} diff --git a/src/app/core/server-check/server-status-guard.service.ts b/src/app/core/server-check/server-status-guard.service.ts new file mode 100644 index 00000000000..b6ed9b9dfa4 --- /dev/null +++ b/src/app/core/server-check/server-status-guard.service.ts @@ -0,0 +1,35 @@ +import { Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot, CanActivateChild, RouterStateSnapshot } from '@angular/router'; + +import { Observable } from 'rxjs'; +import { tap } from 'rxjs/operators'; +import { ServerStatusService } from './server-status.service'; + + +@Injectable({ + providedIn: 'root' +}) +/** + * A guard that checks the server state from the store + * If the server state in the store is not available, redirect to 500 error page + */ +export class ServerStatusGuard implements CanActivateChild { + constructor(protected serverStatusService: ServerStatusService,) { + } + + /** + * True when the server status in the store is available. + */ + canActivateChild( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot): Observable { + return this.serverStatusService.checkServerAvailabilityFromStore() + .pipe( + tap((isAvailableInStore: boolean) => { + if (!isAvailableInStore) { + this.serverStatusService.navigateToInternalServerErrorPage(); + } + }) + ); + } +} diff --git a/src/app/core/server-check/server-status.guard.spec.ts b/src/app/core/server-check/server-status.guard.spec.ts new file mode 100644 index 00000000000..b4753ecd4b5 --- /dev/null +++ b/src/app/core/server-check/server-status.guard.spec.ts @@ -0,0 +1,59 @@ +import { ServerStatusGuard } from './server-status-guard.service'; +import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; + +import { of } from 'rxjs'; +import { ServerStatusService } from './server-status.service'; +import SpyObj = jasmine.SpyObj; + +describe('ServerStatusGuard', () => { + let guard: ServerStatusGuard; + let serverStatusService: SpyObj; + let mockRouteSnapshot: ActivatedRouteSnapshot; + let mockRouterStateSnapshot: RouterStateSnapshot; + + + serverStatusService = jasmine.createSpyObj('ServerStatusService', [ + 'checkServerAvailabilityFromStore', + 'navigateToInternalServerErrorPage', + ]); + + beforeEach(() => { + guard = new ServerStatusGuard(serverStatusService); + mockRouteSnapshot = {} as ActivatedRouteSnapshot; + mockRouterStateSnapshot = {} as RouterStateSnapshot; + }); + + afterEach(() => { + serverStatusService.checkServerAvailabilityFromStore.calls.reset(); + serverStatusService.navigateToInternalServerErrorPage.calls.reset(); + }); + + it('should be created', () => { + expect(guard).toBeTruthy(); + }); + + + describe('canActivate', () => { + it('should allow child activation when server status is available in the store', (done) => { + serverStatusService.checkServerAvailabilityFromStore.and.returnValue(of(true)); + + guard.canActivateChild(mockRouteSnapshot, mockRouterStateSnapshot).subscribe((result) => { + expect(result).toBeTrue(); + expect(serverStatusService.checkServerAvailabilityFromStore).toHaveBeenCalled(); + expect(serverStatusService.navigateToInternalServerErrorPage).not.toHaveBeenCalled(); + done(); + }); + }); + }); + + it('should redirect to the 500 error page when server status is unavailable in the store', (done) => { + serverStatusService.checkServerAvailabilityFromStore.and.returnValue(of(false)); + + guard.canActivateChild(mockRouteSnapshot, mockRouterStateSnapshot).subscribe((result) => { + expect(result).toBeFalse(); + expect(serverStatusService.checkServerAvailabilityFromStore).toHaveBeenCalled(); + expect(serverStatusService.navigateToInternalServerErrorPage).toHaveBeenCalled(); + done(); + }); + }); +}); diff --git a/src/app/core/server-check/server-status.service.spec.ts b/src/app/core/server-check/server-status.service.spec.ts new file mode 100644 index 00000000000..9920be0eb24 --- /dev/null +++ b/src/app/core/server-check/server-status.service.spec.ts @@ -0,0 +1,148 @@ +import { TestBed } from '@angular/core/testing'; +import { of } from 'rxjs'; +import { Router } from '@angular/router'; +import { Store } from '@ngrx/store'; + +import { ServerStatusService } from './server-status.service'; +import { RootDataService } from '../data/root-data.service'; +import { getPageInternalServerErrorRoute } from '../../app-routing-paths'; +import { UpdateServerStatusAction } from '../history/server-status.actions'; +import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { Root } from '../data/root.model'; + +describe('ServerStatusService', () => { + let service: ServerStatusService; + let rootDataServiceMock; + let storeMock; + let routerMock; + + beforeEach(() => { + rootDataServiceMock = jasmine.createSpyObj('RootDataService', ['invalidateRootCache', 'findRoot']); + storeMock = jasmine.createSpyObj('Store', ['select', 'dispatch']); + routerMock = jasmine.createSpyObj('Router', ['navigateByUrl']); + + TestBed.configureTestingModule({ + providers: [ + ServerStatusService, + {provide: RootDataService, useValue: rootDataServiceMock}, + {provide: Store, useValue: storeMock}, + {provide: Router, useValue: routerMock}, + ], + }); + + service = TestBed.inject(ServerStatusService); + }); + + describe('checkServerAvailabilityFromStore', () => { + it('should return true if the server status is available in the store', (done) => { + const serverState = {isAvailable: true}; + storeMock.select.and.returnValue(of(serverState)); + + service.checkServerAvailabilityFromStore().subscribe((result) => { + expect(result).toBeTrue(); + expect(storeMock.select).toHaveBeenCalled(); + expect(rootDataServiceMock.invalidateRootCache).not.toHaveBeenCalled(); + done(); + }); + }); + + it('should verify root availability if the server status is unavailable in the store', (done) => { + const serverState = {isAvailable: false}; + storeMock.select.and.returnValue(of(serverState)); + spyOn(service, 'isRootServerAvailable').and.returnValue(of(true)); + + service.checkServerAvailabilityFromStore().subscribe((result) => { + expect(result).toBeTrue(); + expect(storeMock.select).toHaveBeenCalled(); + expect(rootDataServiceMock.invalidateRootCache).toHaveBeenCalled(); + expect(service.isRootServerAvailable).toHaveBeenCalled(); + done(); + }); + }); + + it('should return false if the root endpoint is unavailable', (done) => { + const serverState = {isAvailable: false}; + storeMock.select.and.returnValue(of(serverState)); + spyOn(service, 'isRootServerAvailable').and.returnValue(of(false)); + + service.checkServerAvailabilityFromStore().subscribe((result) => { + expect(result).toBeFalse(); + expect(storeMock.select).toHaveBeenCalled(); + expect(rootDataServiceMock.invalidateRootCache).toHaveBeenCalled(); + expect(service.isRootServerAvailable).toHaveBeenCalled(); + done(); + }); + }); + }); + + describe('isRootServerAvailable', () => { + it('should return true if the root endpoint is available', (done) => { + rootDataServiceMock.findRoot.and.returnValue(createSuccessfulRemoteDataObject$(new Root())); + service.isRootServerAvailable().subscribe((result) => { + expect(result).toBeTrue(); + expect(rootDataServiceMock.findRoot).toHaveBeenCalled(); + done(); + }); + }); + it('should return false if the root endpoint is not available', (done) => { + rootDataServiceMock.findRoot.and.returnValue(createFailedRemoteDataObject$('error', 500)); + service.isRootServerAvailable().subscribe((result) => { + expect(result).toBeFalse(); + expect(rootDataServiceMock.findRoot).toHaveBeenCalled(); + done(); + }); + }); + }); + + describe('checkAndUpdateServerStatus', () => { + it('should dispatch an UpdateServerStatusAction if the server is down', (done) => { + const request = {method: 'GET'} as any; + const error = {statusCode: 500} as any; + spyOn(service, 'isRootServerAvailable').and.returnValue(of(false)); + + service.checkAndUpdateServerStatus(request, error).subscribe((result) => { + expect(result).toBeFalse(); + expect(service.isRootServerAvailable).toHaveBeenCalled(); + expect(storeMock.dispatch).toHaveBeenCalledWith(new UpdateServerStatusAction(false)); + done(); + }); + }); + + it('should return true if the server is available during the status check', (done) => { + const request = {method: 'GET'} as any; + const error = {statusCode: 500} as any; + spyOn(service, 'isRootServerAvailable').and.returnValue(of(true)); + + service.checkAndUpdateServerStatus(request, error).subscribe((result) => { + expect(result).toBeTrue(); + expect(service.isRootServerAvailable).toHaveBeenCalled(); + expect(storeMock.dispatch).not.toHaveBeenCalled(); + done(); + }); + }); + + it('should return true if the error does not match the specified criteria', (done) => { + const request = {method: 'POST'} as any; + const error = {statusCode: 500} as any; + spyOn(service, 'isRootServerAvailable'); + + service.checkAndUpdateServerStatus(request, error).subscribe((result) => { + expect(result).toBeTrue(); + expect(service.isRootServerAvailable).not.toHaveBeenCalled(); + expect(storeMock.dispatch).not.toHaveBeenCalled(); + done(); + }); + }); + }); + + describe('navigateToInternalServerErrorPage', () => { + it('should invalidate the root cache and navigate to the internal server error page', () => { + const errorPageRoute = getPageInternalServerErrorRoute(); + + service.navigateToInternalServerErrorPage(); + + expect(rootDataServiceMock.invalidateRootCache).toHaveBeenCalled(); + expect(routerMock.navigateByUrl).toHaveBeenCalledWith(errorPageRoute); + }); + }); +}); diff --git a/src/app/core/server-check/server-status.service.ts b/src/app/core/server-check/server-status.service.ts new file mode 100644 index 00000000000..cc33541a07a --- /dev/null +++ b/src/app/core/server-check/server-status.service.ts @@ -0,0 +1,100 @@ +import { Injectable } from '@angular/core'; +import { Observable, of, of as observableOf, switchMap } from 'rxjs'; +import { getFirstCompletedRemoteData } from '../shared/operators'; +import { catchError, map, tap } from 'rxjs/operators'; +import { RemoteData } from '../data/remote-data'; +import { Root } from '../data/root.model'; +import { RootDataService } from '../data/root-data.service'; +import { AppState } from '../../app.reducer'; +import { createSelector, Store } from '@ngrx/store'; +import { UpdateServerStatusAction } from '../history/server-status.actions'; +import { getPageInternalServerErrorRoute } from '../../app-routing-paths'; +import { Router } from '@angular/router'; +import { coreSelector } from '../core.selectors'; +import { CoreState } from '../core-state.model'; +import { RestRequestWithResponseParser } from '../data/rest-request-with-response-parser.model'; +import { RequestError } from '../data/request-error.model'; + +export const getServerStatusState = createSelector(coreSelector, (state: CoreState) => state.serverStatus); + +/** + * Service responsible for checking and storing the server status (whether it is running or not) + */ +@Injectable({ + providedIn: 'root' +}) +export class ServerStatusService { + + constructor( + protected rootDataService: RootDataService, + protected store: Store, + protected router: Router, + ) { + } + + + /** + * Check if the server is running based on the value in the store. + * When the server is not available according to the store, the root endpoint will be checked to verify + * if the server is still down or has come up again. + */ + checkServerAvailabilityFromStore(): Observable { + return this.store.select(getServerStatusState).pipe( + map((state) => state.isAvailable), + switchMap((isAvailable: boolean) => { + if (!isAvailable) { + this.rootDataService.invalidateRootCache(); + return this.isRootServerAvailable(); + } else { + return of(isAvailable); + } + }) + ); + } + + + /** + * Check if the root endpoint is available + */ + isRootServerAvailable() { + return this.rootDataService.findRoot(false).pipe( + getFirstCompletedRemoteData(), + catchError((err) => { + console.error(err); + return observableOf(false); + }), + map((rd: RemoteData) => rd.statusCode === 200) + ); + } + + /** + * When a request with an error is provided, the available of the root endpoint will be checked. + * If the root server is down, update the server status in the store + * Returns whether the server is running or not + * @param request - The request to be checked + * @param error - The error from the request to be checked + */ + checkAndUpdateServerStatus(request: RestRequestWithResponseParser, error: RequestError): Observable { + if ((error.statusCode === 500 || error.statusCode === 0) && request.method === 'GET' && this.router.url !== getPageInternalServerErrorRoute()) { + return this.isRootServerAvailable().pipe( + tap((isAvailable: boolean) => { + if (!isAvailable) { + this.store.dispatch(new UpdateServerStatusAction(false)); + } + }) + ); + } + return observableOf(true); + } + + /** + * Clear the root server cache to ensure that future requests do not use the cached error responses, + * then navigate to the internal server error page + */ + navigateToInternalServerErrorPage() { + this.rootDataService.invalidateRootCache(); + this.router.navigateByUrl(getPageInternalServerErrorRoute()); + } + + +} diff --git a/src/app/root/root.component.html b/src/app/root/root.component.html index 94a5792a6bf..6b9e0ad95d2 100644 --- a/src/app/root/root.component.html +++ b/src/app/root/root.component.html @@ -9,7 +9,7 @@
-
+
diff --git a/src/app/root/root.component.ts b/src/app/root/root.component.ts index 9e43d0bde65..55d9a52920b 100644 --- a/src/app/root/root.component.ts +++ b/src/app/root/root.component.ts @@ -1,26 +1,18 @@ -import { map, startWith, take, tap } from 'rxjs/operators'; -import { Component, Inject, Input, OnInit } from '@angular/core'; +import { map, startWith, switchMap } from 'rxjs/operators'; +import { Component, Inject, Input, OnDestroy, OnInit } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; -import { combineLatest as combineLatestObservable, Observable, of } from 'rxjs'; -import { Store } from '@ngrx/store'; -import { TranslateService } from '@ngx-translate/core'; - -import { MetadataService } from '../core/metadata/metadata.service'; -import { HostWindowState } from '../shared/search/host-window.reducer'; +import { BehaviorSubject, combineLatest as combineLatestObservable, Observable, of, Subscription } from 'rxjs'; import { NativeWindowRef, NativeWindowService } from '../core/services/window.service'; -import { AuthService } from '../core/auth/auth.service'; import { CSSVariableService } from '../shared/sass-helper/css-variable.service'; import { MenuService } from '../shared/menu/menu.service'; import { HostWindowService } from '../shared/host-window.service'; import { ThemeConfig } from '../../config/theme.model'; -import { Angulartics2DSpace } from '../statistics/angulartics/dspace-provider'; import { environment } from '../../environments/environment'; import { slideSidebarPadding } from '../shared/animations/slide'; import { MenuID } from '../shared/menu/menu-id.model'; import { getPageInternalServerErrorRoute } from '../app-routing-paths'; import { hasValueOperator } from '../shared/empty.util'; -import { RootDataService } from '../core/data/root-data.service'; @Component({ selector: 'ds-root', @@ -28,7 +20,7 @@ import { RootDataService } from '../core/data/root-data.service'; styleUrls: ['./root.component.scss'], animations: [slideSidebarPadding], }) -export class RootComponent implements OnInit { +export class RootComponent implements OnInit, OnDestroy { sidebarVisible: Observable; slideSidebarOver: Observable; collapsedSidebarWidth: Observable; @@ -47,21 +39,16 @@ export class RootComponent implements OnInit { */ @Input() shouldShowRouteLoader: boolean; - shouldShowRouteLoader$: Observable; + shouldShowRouteLoader$: BehaviorSubject = new BehaviorSubject(false); + + subs: Subscription[] = []; constructor( @Inject(NativeWindowService) private _window: NativeWindowRef, - private translate: TranslateService, - private store: Store, - private metadata: MetadataService, - private angulartics2DSpace: Angulartics2DSpace, - private authService: AuthService, private router: Router, private cssService: CSSVariableService, private menuService: MenuService, private windowService: HostWindowService, - private rootDataService: RootDataService, - private activateRoute: ActivatedRoute, ) { this.notificationOptions = environment.notifications; } @@ -80,9 +67,30 @@ export class RootComponent implements OnInit { startWith(true), ); - this.shouldShowRouteLoader$ = this.activateRoute.url.pipe( - map((url) => url.join('/')), - map((url) => url === getPageInternalServerErrorRoute()), - ) + if (this.shouldShowRouteLoader && !this.shouldShowFullscreenLoader) { + this.subs.push( + this.router.events.pipe( + map(() => this.router.routerState.root), + switchMap((route: ActivatedRoute) => { + route = this.getCurrentRoute(route); + return route.url; + }), + map((urlSegment) => '/' + urlSegment.join('/')), + map((url) => url === getPageInternalServerErrorRoute()), + ).subscribe((isInternalServerError) => { + this.shouldShowRouteLoader$.next(!isInternalServerError); + })); + } + } + + private getCurrentRoute(route: ActivatedRoute): ActivatedRoute { + while (route.firstChild) { + route = route.firstChild; + } + return route; + } + + ngOnDestroy(): void { + this.subs.forEach((sub) => sub.unsubscribe()); } }