Browse Source

Simple error / notification display mechanism

Lukas Angerer 3 years ago
parent
commit
f4415fa2af

+ 5 - 0
src/RunnersMeet.Client/src/app/app.component.html

@@ -1,4 +1,9 @@
 <h1>{{title}}</h1>
+<ul *ngIf="notificationService.hasMessages">
+	<li *ngFor="let msg of notificationService.messages" [ngClass]="msg.classes">
+		{{ msg.message }} <button *ngIf="msg.isDismissable" (click)="msg.dispose()">Dismiss</button>
+	</li>
+</ul>
 <ul>
 	<li><a [routerLink]="['/']">Home</a></li>
 	<li><a [routerLink]="['/tracks']">Tracks</a></li>

+ 3 - 1
src/RunnersMeet.Client/src/app/app.component.ts

@@ -1,5 +1,6 @@
 import { Component } from '@angular/core';
 import { AuthService } from '@auth0/auth0-angular';
+import { NotificationService } from './notification.service';
 import { PermissionService } from './users/permission.service';
 import { UserState } from './users/user-state';
 
@@ -14,7 +15,8 @@ export class AppComponent {
 
 	public constructor(
 		private readonly authService: AuthService,
-		private readonly permissions: PermissionService
+		private readonly permissions: PermissionService,
+		public readonly notificationService: NotificationService
 	) {
 		this.permissions.isRegistered().then(result => {
 			console.log('AppComponent | isRegistered:', result);

+ 8 - 1
src/RunnersMeet.Client/src/app/app.module.ts

@@ -1,4 +1,4 @@
-import { NgModule } from '@angular/core';
+import { ErrorHandler, NgModule } from '@angular/core';
 import { BrowserModule } from '@angular/platform-browser';
 import { AuthHttpInterceptor, AuthModule } from '@auth0/auth0-angular';
 
@@ -19,6 +19,8 @@ import { ConfigService } from './config.service';
 import { configInitializerProvider } from './initializer';
 import { UsersApiService } from './users/users-api.service';
 import { PermissionService } from './users/permission.service';
+import { GlobalErrorHandler } from './global-error-handler';
+import { NotificationService } from './notification.service';
 
 @NgModule({
 	declarations: [
@@ -47,6 +49,11 @@ import { PermissionService } from './users/permission.service';
 			useClass: AuthHttpInterceptor,
 			multi: true,
 		},
+		NotificationService,
+		{
+			provide: ErrorHandler,
+			useClass: GlobalErrorHandler
+		},
 		UsersApiService,
 		PermissionService,
 		TracksApiService

+ 40 - 0
src/RunnersMeet.Client/src/app/global-error-handler.ts

@@ -0,0 +1,40 @@
+import { ErrorHandler, Injectable, NgZone } from "@angular/core";
+import { NotificationMessage, NotificationService, NotificationType } from "./notification.service";
+
+@Injectable()
+export class GlobalErrorHandler implements ErrorHandler {
+
+	public constructor(
+		private readonly errorReporting: NotificationService,
+		private readonly ngZone: NgZone
+	) {}
+
+	public handleError(error: any): void {
+		if ('rejection' in error) {
+			this.handleProperError(error.rejection);
+		} else if (error instanceof Error) {
+			this.handleProperError(error);
+		} else  {
+			this.handleErrorLike(error);
+		}
+	}
+
+	private handleProperError(error: Error): void {
+		console.error("ERROR:", error);
+		this.ngZone.run(() => {
+			this.errorReporting.reportError(new NotificationMessage(NotificationType.Error, `${error.name}: ${error.message}`, true, 0));
+		});
+	}
+
+	private handleErrorLike(error: unknown): void {
+		let errorType: string = typeof(error);
+		if (errorType === 'object') {
+			errorType = (error as object).constructor.name;
+		}
+		console.error('Unexpected _type_ of error', errorType);
+		console.error('ERROR:', error);
+		this.ngZone.run(() => {
+			this.errorReporting.reportError(new NotificationMessage(NotificationType.Error, `Unknown Error Type: ${error}`, false, 0));
+		});
+	}
+}

+ 52 - 0
src/RunnersMeet.Client/src/app/notification.service.ts

@@ -0,0 +1,52 @@
+import { Injectable, NgZone } from "@angular/core";
+import { Observable, Subject } from "rxjs";
+
+export enum NotificationType {
+	Info = 'Info',
+	Warning = 'Warning',
+	Error = 'Error'
+}
+
+export class NotificationMessage {
+	private readonly _disposeSubject: Subject<void> = new Subject<void>();
+	public readonly onDispose: Observable<void> = this._disposeSubject;
+
+	public readonly classes: { [klass: string]: any; };
+
+	public constructor(
+		public readonly type: NotificationType,
+		public readonly message: string,
+		public readonly isDismissable: boolean,
+		public readonly timeout: number = 0
+	) {
+		this.classes = { [type]: true };
+	}
+
+	public dispose(): void {
+		this._disposeSubject.next();
+		this._disposeSubject.complete();
+	}
+}
+
+@Injectable()
+export class NotificationService {
+	private readonly _messages: Array<NotificationMessage> = [];
+
+	public get messages(): ReadonlyArray<NotificationMessage> {
+		return this._messages;
+	}
+
+	public get hasMessages(): boolean {
+		return this._messages.length > 0;
+	}
+
+	public reportError(message: NotificationMessage): void {
+		message.onDispose.subscribe(() => {
+			const index = this._messages.indexOf(message);
+			if (index >= 0) {
+				this._messages.splice(index, 1);
+			}
+		});
+		this._messages.push(message);
+	}
+}

+ 1 - 1
src/RunnersMeet.Client/src/app/pages/tracks-page/tracks-page.component.ts

@@ -29,7 +29,7 @@ export class TracksPageComponent {
 		this.searchParams = new TrackSearchParams();
 		this.searchParams.owner = this.showMyTracks ? 'me' : undefined;
 		this.searchParams.filter = this.nameFilter ? this.nameFilter : undefined;
-		this.updateTracks()
+		this.updateTracks();
 	}
 
 	public loadMore(): void {