From 67089013afa45c27d5ef38bb3f2f791a63ab56a9 Mon Sep 17 00:00:00 2001 From: FyloZ Date: Tue, 16 Feb 2021 14:34:32 -0500 Subject: [PATCH] =?UTF-8?q?Ajout=20d'un=20syst=C3=A8me=20de=20gestion=20de?= =?UTF-8?q?s=20erreurs=20et=20alertes=20centralis=C3=A9.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/app.component.html | 1 + .../global-alert-handler.component.html | 9 ++ .../global-alert-handler.component.sass | 0 .../global-alert-handler.component.ts | 17 +++ .../modules/shared/service/alert.service.ts | 106 +++++++++++++++++ .../modules/shared/service/error.service.ts | 107 ++++++++++++++++++ src/app/modules/shared/shared.module.ts | 6 +- 7 files changed, 244 insertions(+), 2 deletions(-) create mode 100644 src/app/modules/shared/components/global-alert-handler/global-alert-handler.component.html create mode 100644 src/app/modules/shared/components/global-alert-handler/global-alert-handler.component.sass create mode 100644 src/app/modules/shared/components/global-alert-handler/global-alert-handler.component.ts create mode 100644 src/app/modules/shared/service/alert.service.ts create mode 100644 src/app/modules/shared/service/error.service.ts diff --git a/src/app/app.component.html b/src/app/app.component.html index 4dd83bd..00031de 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -1,4 +1,5 @@ +
diff --git a/src/app/modules/shared/components/global-alert-handler/global-alert-handler.component.html b/src/app/modules/shared/components/global-alert-handler/global-alert-handler.component.html new file mode 100644 index 0000000..19903b7 --- /dev/null +++ b/src/app/modules/shared/components/global-alert-handler/global-alert-handler.component.html @@ -0,0 +1,9 @@ + +
+ {{alert.alert.message}} +
+
diff --git a/src/app/modules/shared/components/global-alert-handler/global-alert-handler.component.sass b/src/app/modules/shared/components/global-alert-handler/global-alert-handler.component.sass new file mode 100644 index 0000000..e69de29 diff --git a/src/app/modules/shared/components/global-alert-handler/global-alert-handler.component.ts b/src/app/modules/shared/components/global-alert-handler/global-alert-handler.component.ts new file mode 100644 index 0000000..d28c3ba --- /dev/null +++ b/src/app/modules/shared/components/global-alert-handler/global-alert-handler.component.ts @@ -0,0 +1,17 @@ +import {Component} from '@angular/core' +import {AlertHandlerComponent, AlertService, AlertType} from '../../service/alert.service' + +@Component({ + selector: 'cre-global-alert-handler', + templateUrl: './global-alert-handler.component.html', + styleUrls: ['./global-alert-handler.component.sass'] +}) +export class GlobalAlertHandlerComponent extends AlertHandlerComponent { + readonly SUCCESS_ALERT_TYPE = AlertType.Success + readonly WARNING_ALERT_TYPE = AlertType.Warning + readonly ERROR_ALERT_TYPE = AlertType.Error + + constructor(alertService: AlertService) { + super(alertService) + } +} diff --git a/src/app/modules/shared/service/alert.service.ts b/src/app/modules/shared/service/alert.service.ts new file mode 100644 index 0000000..b8e2922 --- /dev/null +++ b/src/app/modules/shared/service/alert.service.ts @@ -0,0 +1,106 @@ +import {Injectable} from '@angular/core' +import {interval} from 'rxjs' + +@Injectable({ + providedIn: 'root' +}) +export class AlertService { + private alertQueue: Alert[] = [] + private activeHandler: AlertHandlerComponent + + constructor() { + // interval(500).subscribe({next: i => this.pushError('Test error ' + i)}) + } + + pushSuccess(message: String) { + this.enqueue(AlertType.Success, message) + } + + pushWarning(message: String) { + this.enqueue(AlertType.Warning, message) + } + + pushError(message: String) { + this.enqueue(AlertType.Error, message) + } + + set activeAlertHandlerComponent(handler: AlertHandlerComponent) { + this.activeHandler = handler + } + + private enqueue(type: AlertType, message: String) { + const alert = {type, message} + if (this.activeHandler && !this.activeHandler.isBufferFull) { + if (this.alertQueue.length == 0) { + this.activeHandler.pushAlert(alert) + } else { + this.activeHandler.pushAlert(this.dequeue()) + this.alertQueue.unshift(alert) + } + } else { + this.alertQueue.unshift(alert) + } + } + + private dequeue(): Alert { + return this.alertQueue.pop() + } +} + +/** + * An alert handler component is a component that will show the alerts pushed by the alert system to the user. + */ +export abstract class AlertHandlerComponent { + protected static readonly DEFAULT_ALERT_BUFFER_SIZE = 3 + protected static readonly DEFAULT_ALERT_DURATION = 5 + + alertBuffer = new Array<{ alert: Alert, time: number }>() + protected alertBufferSize: number = AlertHandlerComponent.DEFAULT_ALERT_BUFFER_SIZE + protected alertDuration: number = AlertHandlerComponent.DEFAULT_ALERT_DURATION + protected alertDurationCounter = 0 + + protected constructor( + protected alertService: AlertService + ) { + alertService.activeAlertHandlerComponent = this + + interval(1000).subscribe({ + next: () => { + this.alertDurationCounter++ + if (this.alertBuffer.length > 1) { + this.alertBuffer + .filter(a => a.time + this.alertDuration < this.alertDurationCounter) + .map(a => this.alertBuffer.indexOf(a)) + .forEach(i => this.alertBuffer.splice(i, 1)) + } + } + }) + } + + pushAlert(alert: Alert) { + this.alertBuffer.unshift({alert: alert, time: this.alertDurationCounter}) + } + + get isBufferFull(): boolean { + return this.alertBuffer.length >= this.alertBufferSize + } +} + +/** + * An alert is a message that will be shown to the user. + * + * An alert can be a success alert, a warning alert of an error alert. + */ +class Alert { + constructor( + public type: AlertType, + public message: String + ) { + } +} + +export enum AlertType { + Success, + Warning, + Error +} diff --git a/src/app/modules/shared/service/error.service.ts b/src/app/modules/shared/service/error.service.ts new file mode 100644 index 0000000..45c61e7 --- /dev/null +++ b/src/app/modules/shared/service/error.service.ts @@ -0,0 +1,107 @@ +import {Injectable} from '@angular/core' +import {AlertService} from './alert.service' +import {AppState} from '../app-state' + +@Injectable({ + providedIn: 'root' +}) +export class ErrorService { + private static readonly UNKNOWN_ERROR_MESSAGE = 'Une erreur inconnue est survenue' + private defaultHandledErrorModels: ErrorModel[] = [{ + filter: error => error.status === 0 && error.statusText === 'Unknown Error' || error.status === 502, + consumer: () => this.appState.isServerOnline = false + }, { + filter: error => error.status === 400, + consumer: error => console.error(error), + messageProducer: () => 'Certaines informations dans la requête étaient invalides' + }, { + filter: error => error.status === 401, + messageProducer: () => 'Vous devez être connecté pour effectuer cette action' + }, { + filter: error => error.status === 403, + messageProducer: () => 'Vous n\'avez pas la permission d\'effectuer cette action' + }, { + filter: error => error.status === 404, + messageProducer: () => 'La resource demandée n\'a pas été trouvée' + }, { + filter: error => error.status === 500, + messageProducer: () => ErrorService.UNKNOWN_ERROR_MESSAGE + }] + + private activeHandler: ErrorHandler + + constructor( + private alertService: AlertService, + private appState: AppState + ) { + } + + handleError(error: any) { + if (!this.activeHandler) { + console.error('An error occurred but no handler was set') + } + + let matchingModels = this.activeHandler.handledErrorModels.filter(m => m.filter(error)) // Find error models whose filter matches the current error + + if (!matchingModels || matchingModels.length == 0) { // If none are found, search in defaults handlers + matchingModels = this.defaultHandledErrorModels.filter(m => m.filter(error)) + } + + if (!matchingModels || matchingModels.length == 0) { // If still none are found, handle as an unknown error + this.consumeUnknownError(error) + return + } + + matchingModels.forEach(m => { + if (m.consumer || m.messageProducer) { + if (m.consumer) { + m.consumer(error) + } + if (m.messageProducer) { + this.alertService.pushError(m.messageProducer(error)) + } + } else { + console.error('An error model has no consumer or message') + } + }) + } + + consumeUnknownError(error: any) { + console.error(error) + this.alertService.pushError(ErrorService.UNKNOWN_ERROR_MESSAGE) + } + + set activeErrorHandler(handler: ErrorHandler) { + this.activeHandler = handler + } +} + +/** + * An error handler, defining models of errors that this type will handle + */ +export abstract class ErrorHandler { + abstract handledErrorModels: ErrorModel[] + + protected constructor( + protected errorService: ErrorService + ) { + this.errorService.activeErrorHandler = this + } +} + +/** + * An error model define how errors matching its filter will be handled. + * + * The consumer will consume matching errors when they occurs. + * The message producer returns a string that will be pushed to the alert system. + * + * To work correctly a model must define at least one handler (consumer or message producer). + */ +export class ErrorModel { + constructor( + public filter: (error: any) => Boolean, + public consumer?: (error: any) => void, + public messageProducer?: (error: any) => String + ) { + } +} diff --git a/src/app/modules/shared/shared.module.ts b/src/app/modules/shared/shared.module.ts index 62c3d95..8ea2b4a 100644 --- a/src/app/modules/shared/shared.module.ts +++ b/src/app/modules/shared/shared.module.ts @@ -27,10 +27,11 @@ import {MatSelectModule} from "@angular/material/select"; import {MatOptionModule} from "@angular/material/core"; import {MaterialFileInputModule} from "ngx-material-file-input"; import { FileButtonComponent } from './file-button/file-button.component'; +import { GlobalAlertHandlerComponent } from './components/global-alert-handler/global-alert-handler.component'; @NgModule({ - declarations: [HeaderComponent, EmployeeInfoComponent, LabeledIconComponent, ConfirmBoxComponent, PermissionsListComponent, PermissionsFieldComponent, NavComponent, EntityListComponent, EntityAddComponent, EntityEditComponent, FileButtonComponent], + declarations: [HeaderComponent, EmployeeInfoComponent, LabeledIconComponent, ConfirmBoxComponent, PermissionsListComponent, PermissionsFieldComponent, NavComponent, EntityListComponent, EntityAddComponent, EntityEditComponent, FileButtonComponent, GlobalAlertHandlerComponent], exports: [ CommonModule, HttpClientModule, @@ -55,7 +56,8 @@ import { FileButtonComponent } from './file-button/file-button.component'; EntityListComponent, EntityAddComponent, EntityEditComponent, - FileButtonComponent + FileButtonComponent, + GlobalAlertHandlerComponent ], imports: [ MatTabsModule,