Ajout d'un système de gestion des erreurs et alertes centralisé.

This commit is contained in:
FyloZ 2021-02-16 14:34:32 -05:00
parent 93929718d6
commit 67089013af
7 changed files with 244 additions and 2 deletions

View File

@ -1,4 +1,5 @@
<cre-header></cre-header>
<cre-global-alert-handler></cre-global-alert-handler>
<div>
<div class="dark-background"></div>
<router-outlet></router-outlet>

View File

@ -0,0 +1,9 @@
<ng-container *ngFor="let alert of alertBuffer">
<div
class="alert"
[class.alert-success]="alert.alert.type === SUCCESS_ALERT_TYPE"
[class.alert-warning]="alert.alert.type === WARNING_ALERT_TYPE"
[class.alert-danger]="alert.alert.type === ERROR_ALERT_TYPE">
{{alert.alert.message}}
</div>
</ng-container>

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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
) {
}
}

View File

@ -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,