From 90287baf62e8a5cfbec742b6947352c56c255bdb Mon Sep 17 00:00:00 2001 From: Dan Percic Date: Sun, 19 Sep 2021 23:42:46 +0300 Subject: [PATCH] add server error interceptor and update full page error --- src/assets/icons/offline.svg | 1 + src/assets/styles/_full-pages.scss | 9 ++-- .../full-page-error.component.html | 47 ++++++++++++----- .../full-page-error.component.scss | 37 +++++++++---- .../full-page-error.component.ts | 8 +++ src/lib/error/index.ts | 2 + src/lib/error/max-retries.token.ts | 3 ++ src/lib/error/server-error-interceptor.ts | 52 +++++++++++++++++++ src/lib/icons/icons.module.ts | 1 + src/lib/listing/sync-width.directive.ts | 2 +- .../utils/decorators/debounce.decorator.ts | 2 +- 11 files changed, 134 insertions(+), 30 deletions(-) create mode 100644 src/assets/icons/offline.svg create mode 100644 src/lib/error/max-retries.token.ts create mode 100644 src/lib/error/server-error-interceptor.ts diff --git a/src/assets/icons/offline.svg b/src/assets/icons/offline.svg new file mode 100644 index 0000000..d5c6610 --- /dev/null +++ b/src/assets/icons/offline.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/styles/_full-pages.scss b/src/assets/styles/_full-pages.scss index 9c9c6a4..90a85ae 100644 --- a/src/assets/styles/_full-pages.scss +++ b/src/assets/styles/_full-pages.scss @@ -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; } diff --git a/src/lib/error/full-page-error/full-page-error.component.html b/src/lib/error/full-page-error/full-page-error.component.html index e55ee4d..1ffdcf7 100644 --- a/src/lib/error/full-page-error/full-page-error.component.html +++ b/src/lib/error/full-page-error/full-page-error.component.html @@ -1,15 +1,36 @@ -
-
- -
-
{{ error.message }}
- -
+ +
+ + +
+ + +
+
+ + +
+ +
+ + +
+ +
{{ error.message }}
+ + +
+
diff --git a/src/lib/error/full-page-error/full-page-error.component.scss b/src/lib/error/full-page-error/full-page-error.component.scss index 8791f01..6638559 100644 --- a/src/lib/error/full-page-error/full-page-error.component.scss +++ b/src/lib/error/full-page-error/full-page-error.component.scss @@ -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; +} diff --git a/src/lib/error/full-page-error/full-page-error.component.ts b/src/lib/error/full-page-error/full-page-error.component.ts index dcc4fce..43862b9 100644 --- a/src/lib/error/full-page-error/full-page-error.component.ts +++ b/src/lib/error/full-page-error/full-page-error.component.ts @@ -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 { diff --git a/src/lib/error/index.ts b/src/lib/error/index.ts index 9a01d8a..42fbcd1 100644 --- a/src/lib/error/index.ts +++ b/src/lib/error/index.ts @@ -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'; diff --git a/src/lib/error/max-retries.token.ts b/src/lib/error/max-retries.token.ts new file mode 100644 index 0000000..b2ad58a --- /dev/null +++ b/src/lib/error/max-retries.token.ts @@ -0,0 +1,3 @@ +import { InjectionToken } from '@angular/core'; + +export const MAX_RETRIES_ON_SERVER_ERROR = new InjectionToken('Number of retries before giving up'); diff --git a/src/lib/error/server-error-interceptor.ts b/src/lib/error/server-error-interceptor.ts new file mode 100644 index 0000000..913805d --- /dev/null +++ b/src/lib/error/server-error-interceptor.ts @@ -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> { + 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, next: HttpHandler): Observable> { + 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) + ); + } +} diff --git a/src/lib/icons/icons.module.ts b/src/lib/icons/icons.module.ts index 8c39496..85445a7 100644 --- a/src/lib/icons/icons.module.ts +++ b/src/lib/icons/icons.module.ts @@ -18,6 +18,7 @@ export class IqserIconsModule { 'edit', 'failure', 'help-outline', + 'offline', 'refresh', 'search', 'sort-asc', diff --git a/src/lib/listing/sync-width.directive.ts b/src/lib/listing/sync-width.directive.ts index c7cde8c..d2f4693 100644 --- a/src/lib/listing/sync-width.directive.ts +++ b/src/lib/listing/sync-width.directive.ts @@ -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); } diff --git a/src/lib/utils/decorators/debounce.decorator.ts b/src/lib/utils/decorators/debounce.decorator.ts index 0da4c4d..8be812b 100644 --- a/src/lib/utils/decorators/debounce.decorator.ts +++ b/src/lib/utils/decorators/debounce.decorator.ts @@ -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;