Error handling:

- catch QuotaExceededError
- add generic error message to the spinner
This commit is contained in:
Finn Stutzenstein 2021-03-01 13:17:07 +01:00 committed by Emanuel Schütze
parent a450a1dff5
commit 619a698272
9 changed files with 96 additions and 16 deletions

View File

@ -1,5 +1,5 @@
import { HttpClientModule, HttpClientXsrfModule } from '@angular/common/http'; import { HttpClientModule, HttpClientXsrfModule } from '@angular/common/http';
import { APP_INITIALIZER, NgModule } from '@angular/core'; import { APP_INITIALIZER, ErrorHandler, NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser'; import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ServiceWorkerModule } from '@angular/service-worker'; import { ServiceWorkerModule } from '@angular/service-worker';
@ -11,6 +11,7 @@ import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component'; import { AppComponent } from './app.component';
import { CoreModule } from './core/core.module'; import { CoreModule } from './core/core.module';
import { environment } from '../environments/environment'; import { environment } from '../environments/environment';
import { ErrorService } from './core/core-services/error.service';
import { httpInterceptorProviders } from './core/core-services/http-interceptors'; import { httpInterceptorProviders } from './core/core-services/http-interceptors';
import { LoginModule } from './site/login/login.module'; import { LoginModule } from './site/login/login.module';
import { OpenSlidesTranslateModule } from './core/translate/openslides-translate-module'; import { OpenSlidesTranslateModule } from './core/translate/openslides-translate-module';
@ -47,7 +48,8 @@ export function AppLoaderFactory(appLoadService: AppLoadService): () => Promise<
], ],
providers: [ providers: [
{ provide: APP_INITIALIZER, useFactory: AppLoaderFactory, deps: [AppLoadService], multi: true }, { provide: APP_INITIALIZER, useFactory: AppLoaderFactory, deps: [AppLoadService], multi: true },
httpInterceptorProviders httpInterceptorProviders,
{ provide: ErrorHandler, useClass: ErrorService }
], ],
bootstrap: [AppComponent] bootstrap: [AppComponent]
}) })

View File

@ -5,6 +5,7 @@ import { Observable, Subject } from 'rxjs';
import { BaseModel, ModelConstructor } from '../../shared/models/base/base-model'; import { BaseModel, ModelConstructor } from '../../shared/models/base/base-model';
import { CollectionStringMapperService } from './collection-string-mapper.service'; import { CollectionStringMapperService } from './collection-string-mapper.service';
import { Deferred } from '../promises/deferred'; import { Deferred } from '../promises/deferred';
import { OpenSlidesStatusService } from './openslides-status.service';
import { RelationCacheService } from './relation-cache.service'; import { RelationCacheService } from './relation-cache.service';
import { StorageService } from './storage.service'; import { StorageService } from './storage.service';
@ -338,7 +339,8 @@ export class DataStoreService {
public constructor( public constructor(
private storageService: StorageService, private storageService: StorageService,
private modelMapper: CollectionStringMapperService, private modelMapper: CollectionStringMapperService,
private DSUpdateManager: DataStoreUpdateManagerService private DSUpdateManager: DataStoreUpdateManagerService,
private statusService: OpenSlidesStatusService
) {} ) {}
/** /**
@ -662,8 +664,16 @@ export class DataStoreService {
*/ */
public async flushToStorage(changeId: number): Promise<void> { public async flushToStorage(changeId: number): Promise<void> {
this._maxChangeId = changeId; this._maxChangeId = changeId;
try {
await this.storageService.set(DataStoreService.cachePrefix + 'DS', this.jsonStore); await this.storageService.set(DataStoreService.cachePrefix + 'DS', this.jsonStore);
await this.storageService.set(DataStoreService.cachePrefix + 'maxChangeId', changeId); await this.storageService.set(DataStoreService.cachePrefix + 'maxChangeId', changeId);
} catch (e) {
if (e?.name === 'QuotaExceededError') {
this.statusService.setTooLessLocalStorage();
} else {
throw e;
}
}
} }
public print(): void { public print(): void {

View File

@ -0,0 +1,36 @@
import { ErrorHandler, Injectable } from '@angular/core';
import { OpenSlidesStatusService } from './openslides-status.service';
@Injectable({
providedIn: 'root'
})
export class ErrorService extends ErrorHandler {
// TODO: This service cannot be injected into other services since it is constructed twice.
public constructor(private statusService: OpenSlidesStatusService) {
super();
}
public handleError(error: any): void {
const errorInformation = {
error,
name: this.guessName(error)
};
this.statusService.currentError.next(errorInformation);
super.handleError(error);
}
private guessName(error: any): string | null {
if (!error) {
return;
}
if (error.rejection?.name) {
return error.rejection.name;
}
if (error.name) {
return error.name;
}
}
}

View File

@ -6,6 +6,11 @@ import { History } from 'app/shared/models/core/history';
import { BannerDefinition, BannerService } from '../ui-services/banner.service'; import { BannerDefinition, BannerService } from '../ui-services/banner.service';
import { Deferred } from '../promises/deferred'; import { Deferred } from '../promises/deferred';
export interface ErrorInformation {
error: any;
name?: string;
}
/** /**
* Holds information about OpenSlides. This is not included into other services to * Holds information about OpenSlides. This is not included into other services to
* avoid circular dependencies. * avoid circular dependencies.
@ -18,10 +23,12 @@ export class OpenSlidesStatusService {
* in History mode, saves the history point. * in History mode, saves the history point.
*/ */
private history: History = null; private history: History = null;
private bannerDefinition: BannerDefinition = { private historyBanner: BannerDefinition = {
type: 'history' type: 'history'
}; };
private tooLessLocalStorage = false;
/** /**
* Returns, if OpenSlides is in the history mode. * Returns, if OpenSlides is in the history mode.
*/ */
@ -35,6 +42,8 @@ export class OpenSlidesStatusService {
public isPrioritizedClient = false; public isPrioritizedClient = false;
public readonly currentError = new BehaviorSubject<ErrorInformation | null>(null);
private _stable = new Deferred(); private _stable = new Deferred();
private _bootedSubject = new BehaviorSubject<boolean>(false); private _bootedSubject = new BehaviorSubject<boolean>(false);
@ -63,7 +72,7 @@ export class OpenSlidesStatusService {
*/ */
public enterHistoryMode(history: History): void { public enterHistoryMode(history: History): void {
this.history = history; this.history = history;
this.banner.addBanner(this.bannerDefinition); this.banner.addBanner(this.historyBanner);
} }
/** /**
@ -71,6 +80,13 @@ export class OpenSlidesStatusService {
*/ */
public leaveHistoryMode(): void { public leaveHistoryMode(): void {
this.history = null; this.history = null;
this.banner.removeBanner(this.bannerDefinition); this.banner.removeBanner(this.historyBanner);
}
public setTooLessLocalStorage(): void {
if (!this.tooLessLocalStorage) {
this.tooLessLocalStorage = true;
this.banner.addBanner({ type: 'tooLessLocalStorage' });
}
} }
} }

View File

@ -30,10 +30,7 @@ export class BannerService {
public activeBanners: BehaviorSubject<BannerDefinition[]> = new BehaviorSubject<BannerDefinition[]>([]); public activeBanners: BehaviorSubject<BannerDefinition[]> = new BehaviorSubject<BannerDefinition[]>([]);
public constructor(/*translate: TranslateService, */ offlineBroadcastService: OfflineBroadcastService) { public constructor(offlineBroadcastService: OfflineBroadcastService) {
/*translate.onLangChange.subscribe(() => {
this.offlineBannerDefinition.text = translate.instant(this.offlineBannerDefinition.text);
});*/
offlineBroadcastService.isOfflineObservable.subscribe(offline => { offlineBroadcastService.isOfflineObservable.subscribe(offline => {
if (offline) { if (offline) {
this.addBanner(this.offlineBannerDefinition); this.addBanner(this.offlineBannerDefinition);

View File

@ -3,6 +3,7 @@
class="banner" class="banner"
[ngClass]="[ [ngClass]="[
banner.type === 'history' ? 'history-mode-indicator' : '', banner.type === 'history' ? 'history-mode-indicator' : '',
banner.type === 'tooLessLocalStorage' ? 'too-less-local-storage-indicator' : '',
banner.class ? banner.class : '', banner.class ? banner.class : '',
banner.largerOnMobileView ? 'larger-on-mobile' : '' banner.largerOnMobileView ? 'larger-on-mobile' : ''
]" ]"
@ -13,6 +14,10 @@
<span>({{ getHistoryTimestamp() }})</span> <span>({{ getHistoryTimestamp() }})</span>
<a (click)="timeTravel.resumeTime()">{{ 'Exit' | translate }}</a> <a (click)="timeTravel.resumeTime()">{{ 'Exit' | translate }}</a>
</ng-container> </ng-container>
<ng-container *ngSwitchCase="'tooLessLocalStorage'">
<span>{{ "QuotaExceededError: The local storage's quota is too low" | translate }}</span>
<a href="https://support.openslides.com/help/de-de/7/57" target="_blank">{{ 'Click here for more information' | translate }}</a>
</ng-container>
<ng-container *ngSwitchDefault> <ng-container *ngSwitchDefault>
<a class="banner-link" [routerLink]="banner.link" [style.cursor]="banner.link ? 'pointer' : 'default'"> <a class="banner-link" [routerLink]="banner.link" [style.cursor]="banner.link ? 'pointer' : 'default'">
<mat-icon>{{ banner.icon }}</mat-icon> <mat-icon>{{ banner.icon }}</mat-icon>

View File

@ -40,7 +40,8 @@
} }
} }
.history-mode-indicator { .history-mode-indicator,
.too-less-local-storage-indicator {
background: repeating-linear-gradient(45deg, #ffee00, #ffee00 10px, #070600 10px, #000000 20px); background: repeating-linear-gradient(45deg, #ffee00, #ffee00 10px, #070600 10px, #000000 20px);
span, span,

View File

@ -2,7 +2,11 @@
<div class="spinner-container"> <div class="spinner-container">
<div> <div>
<div class="spinner-component"></div> <div class="spinner-component"></div>
<div class="spinner-text">{{ text }}</div> <div *ngIf="!error" class="spinner-text">{{ text }}</div>
<div *ngIf="error" class="spinner-text">
{{ 'An error happened' | translate}}{{ error.name ? ': ' + error.name : ''}}.<br>
{{ 'Please contact your system administrator.' | translate }}
</div>
</div> </div>
</div> </div>
</os-overlay> </os-overlay>

View File

@ -4,6 +4,7 @@ import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { ErrorInformation, OpenSlidesStatusService } from 'app/core/core-services/openslides-status.service';
import { OverlayService } from 'app/core/ui-services/overlay.service'; import { OverlayService } from 'app/core/ui-services/overlay.service';
/** /**
@ -35,6 +36,8 @@ export class GlobalSpinnerComponent implements OnInit, OnDestroy {
*/ */
private LOADING = this.translate.instant('Loading data. Please wait ...'); private LOADING = this.translate.instant('Loading data. Please wait ...');
public error: ErrorInformation | null = null;
/** /**
* Constructor * Constructor
* *
@ -45,8 +48,14 @@ export class GlobalSpinnerComponent implements OnInit, OnDestroy {
public constructor( public constructor(
private overlayService: OverlayService, private overlayService: OverlayService,
protected translate: TranslateService, protected translate: TranslateService,
private cd: ChangeDetectorRef private cd: ChangeDetectorRef,
) {} private statusService: OpenSlidesStatusService
) {
this.statusService.currentError.subscribe(error => {
this.error = error;
this.cd.markForCheck();
});
}
/** /**
* Init method * Init method