add server error interceptor and update full page error

This commit is contained in:
Dan Percic 2021-09-19 23:42:46 +03:00
parent 6c0f123bd9
commit 90287baf62
11 changed files with 134 additions and 30 deletions

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M21 11l2-2c-3.73-3.73-8.87-5.15-13.7-4.31l2.58 2.58c3.3-.02 6.61 1.22 9.12 3.73zm-2 2c-1.08-1.08-2.36-1.85-3.72-2.33l3.02 3.02.7-.69zM9 17l3 3 3-3c-1.65-1.66-4.34-1.66-6 0zM3.41 1.64L2 3.05 5.05 6.1C3.59 6.83 2.22 7.79 1 9l2 2c1.23-1.23 2.65-2.16 4.17-2.78l2.24 2.24C7.79 10.89 6.27 11.74 5 13l2 2c1.35-1.35 3.11-2.04 4.89-2.06l7.08 7.08 1.41-1.41L3.41 1.64z"/></svg>

After

Width:  |  Height:  |  Size: 517 B

View File

@ -1,17 +1,14 @@
@import 'variables';
@use 'variables';
.full-page-section,
.full-page-content {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
inset: 0;
}
.full-page-section {
opacity: 0.7;
background: $white;
background: variables.$white;
z-index: 900;
}

View File

@ -1,15 +1,36 @@
<ng-container *ngIf="errorService.error$ | async as error">
<section class="full-page-section"></section>
<section class="full-page-content">
<mat-icon svgIcon="iqser:failure"></mat-icon>
<div class="heading-l mt-24" translate="error.title"></div>
<div class="mt-16 error">{{ error.message }}</div>
<iqser-icon-button
(action)="reload()"
[label]="'error.reload' | translate"
[type]="iconButtonTypes.primary"
class="mt-20"
icon="iqser:refresh"
></iqser-icon-button>
</section>
<ng-container *ngIf="error.status === 0; else serverError">
<div class="offline-box flex-align-items-center" [@animateOpenClose]="'open'">
<mat-icon svgIcon="iqser:offline"></mat-icon>
<div class="heading-l ml-14" [translate]="'error.offline'"></div>
<iqser-circle-button
(action)="errorService.set(undefined)"
[tooltip]="'error.close' | translate"
icon="iqser:close"
tooltipPosition="below"
></iqser-circle-button>
</div>
</ng-container>
<ng-template #serverError>
<section class="full-page-section"></section>
<section class="full-page-content flex-align-items-center">
<mat-icon svgIcon="iqser:failure"></mat-icon>
<div class="heading-l mt-24" [translate]="'error.title'"></div>
<div class="mt-16 error">{{ error.message }}</div>
<iqser-icon-button
(action)="reload()"
[label]="'error.reload' | translate"
[type]="iconButtonTypes.primary"
class="mt-20"
icon="iqser:refresh"
></iqser-icon-button>
</section>
</ng-template>
</ng-container>

View File

@ -1,13 +1,32 @@
@import '../../../assets/styles/variables';
@use '../../../assets/styles/variables';
.offline-box {
position: fixed;
bottom: 20px;
right: 20px;
height: 40px;
width: 300px;
background: variables.$white;
border: 1px solid variables.$separator;
border-radius: 10px;
padding: 14px;
> mat-icon {
opacity: 0.3;
}
> iqser-circle-button {
flex-grow: 1;
justify-content: flex-end;
}
}
.full-page-section {
opacity: 0.95;
}
.full-page-content {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
> mat-icon {
@ -16,12 +35,12 @@
opacity: 0.1;
}
.heading-l {
color: $grey-7;
font-weight: initial;
}
.error {
color: $red-1;
color: variables.$primary;
}
}
:is(.offline-box, .full-page-content) .heading-l {
color: #9398a0;
font-weight: initial;
}

View File

@ -1,11 +1,19 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { IconButtonTypes } from '../../buttons';
import { ErrorService } from '../error.service';
import { animate, state, style, transition, trigger } from '@angular/animations';
@Component({
selector: 'iqser-full-page-error',
templateUrl: './full-page-error.component.html',
styleUrls: ['./full-page-error.component.scss'],
animations: [
trigger('animateOpenClose', [
state('open', style({ bottom: '20px' })),
state('void', style({ bottom: '-70px' })),
transition('* => open, open => void', animate('1s ease-in-out'))
])
],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class FullPageErrorComponent {

View File

@ -1,2 +1,4 @@
export * from './error.service';
export * from './max-retries.token';
export * from './server-error-interceptor';
export * from './full-page-error/full-page-error.component';

View File

@ -0,0 +1,3 @@
import { InjectionToken } from '@angular/core';
export const MAX_RETRIES_ON_SERVER_ERROR = new InjectionToken<number>('Number of retries before giving up');

View File

@ -0,0 +1,52 @@
import { HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
import { Inject, Injectable, Optional } from '@angular/core';
import { ErrorService } from '@iqser/common-ui';
import { MonoTypeOperatorFunction, Observable, throwError, timer } from 'rxjs';
import { catchError, mergeMap, retryWhen, tap } from 'rxjs/operators';
import { MAX_RETRIES_ON_SERVER_ERROR } from './max-retries.token';
function updateSeconds(seconds: number) {
if (seconds === 0 || seconds === 1) {
return seconds + 1;
} else {
return seconds * seconds;
}
}
function backoffOnServerError(maxRetries = 3): MonoTypeOperatorFunction<HttpEvent<unknown>> {
let seconds = 0;
return retryWhen(attempts =>
attempts.pipe(
tap(() => (seconds = updateSeconds(seconds))),
mergeMap((error: HttpErrorResponse, index) => {
if ((error.status <= 500 && error.status !== 0) || index === maxRetries) {
return throwError(error);
} else {
console.error('An error occurred: ', error);
console.error(`Retrying in ${seconds} seconds...`);
return timer(seconds * 1000);
}
})
)
);
}
@Injectable()
export class ServerErrorInterceptor implements HttpInterceptor {
constructor(
private readonly _errorService: ErrorService,
@Optional() @Inject(MAX_RETRIES_ON_SERVER_ERROR) private readonly _maxRetries: number
) {}
intercept(req: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
return next.handle(req).pipe(
catchError((error: HttpErrorResponse) => {
if (error.status >= 500 || error.status === 0) {
this._errorService.set(error);
}
return throwError(error);
}),
backoffOnServerError(this._maxRetries || 3)
);
}
}

View File

@ -18,6 +18,7 @@ export class IqserIconsModule {
'edit',
'failure',
'help-outline',
'offline',
'refresh',
'search',
'sort-asc',

View File

@ -8,7 +8,7 @@ export class SyncWidthDirective implements OnDestroy {
private readonly _interval: number;
constructor(private readonly _elementRef: ElementRef) {
this._interval = setInterval(() => {
this._interval = window.setInterval(() => {
this._matchWidth();
}, 1000);
}

View File

@ -6,7 +6,7 @@ export function Debounce(delay = 300): MethodDecorator {
descriptorCopy.value = function _new(...args: unknown[]) {
clearTimeout(timeout);
timeout = setTimeout(() => (descriptor.value as (...params: unknown[]) => unknown).apply(this, args), delay);
timeout = window.setTimeout(() => (descriptor.value as (...params: unknown[]) => unknown).apply(this, args), delay);
};
return descriptorCopy;