From ca957326b3514c7bc158fd80c9f65d1967d431d0 Mon Sep 17 00:00:00 2001 From: Dan Percic Date: Wed, 10 Nov 2021 00:54:07 +0200 Subject: [PATCH] change offline indicator design & improve offline state detection --- src/lib/error/error.service.ts | 18 +++---- .../full-page-error.component.html | 47 +++++++------------ .../full-page-error.component.scss | 40 ++++++++++------ .../full-page-error.component.ts | 20 ++++++-- src/lib/error/server-error-interceptor.ts | 19 ++++---- 5 files changed, 77 insertions(+), 67 deletions(-) diff --git a/src/lib/error/error.service.ts b/src/lib/error/error.service.ts index 2f2e878..dad801b 100644 --- a/src/lib/error/error.service.ts +++ b/src/lib/error/error.service.ts @@ -1,17 +1,19 @@ import { Injectable } from '@angular/core'; -import { BehaviorSubject, Observable } from 'rxjs'; -import { HttpErrorResponse } from '@angular/common/http'; +import { BehaviorSubject } from 'rxjs'; +import { HttpErrorResponse, HttpStatusCode } from '@angular/common/http'; import { LoadingService } from '../loading'; -import { filter } from 'rxjs/operators'; +import { filter, mapTo } from 'rxjs/operators'; import { NavigationStart, Router } from '@angular/router'; +const isBadGateway = (error?: HttpErrorResponse) => error?.status === HttpStatusCode.BadGateway; + @Injectable({ providedIn: 'root' }) export class ErrorService { - readonly error$: Observable; - private readonly _errorEvent$ = new BehaviorSubject(undefined); + readonly error$ = new BehaviorSubject(undefined); + readonly badGatewayError$ = this.error$.pipe(filter(isBadGateway), mapTo(new Event('offline'))); + readonly serverError$ = this.error$.pipe(filter(error => !isBadGateway(error))); constructor(private readonly _loadingService: LoadingService, private readonly _router: Router) { - this.error$ = this._errorEvent$.asObservable(); _router.events.pipe(filter(event => event instanceof NavigationStart)).subscribe(() => { this.clear(); }); @@ -19,10 +21,10 @@ export class ErrorService { set(error: HttpErrorResponse): void { this._loadingService.stop(); - this._errorEvent$.next(error); + this.error$.next(error); } clear(): void { - this._errorEvent$.next(undefined); + this.error$.next(undefined); } } 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 a12c641..2b6d7cb 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,36 +1,23 @@ - - -
- +
+ +
-
+ +
- -
-
+
+ - -
+
-
- +
{{ 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 d1f3986..cc2b1f9 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,22 +1,34 @@ -.offline-box { +@use 'sass:math'; + +.indicator { + $width: 160px; + position: fixed; - bottom: 20px; - right: 20px; + top: 0; + right: 50%; + left: 50%; height: 40px; - width: 300px; - background: var(--iqser-white); - border: 1px solid var(--iqser-separator); - border-radius: 10px; - padding: 14px; + margin-left: -(math.div($width, 2)); + width: $width; + border-radius: 8px; + z-index: 5; - > mat-icon { - opacity: 0.3; + span { + color: var(--iqser-grey-1); + text-align: center; + line-height: 18px; + font-weight: 600; + height: 18px; + width: 100%; } +} - > iqser-circle-button { - flex-grow: 1; - justify-content: flex-end; - } +.offline { + background: var(--iqser-yellow-2); +} + +.online { + background: var(--iqser-green-2); } .full-page-section { 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 80c14e0..8c90a6e 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 @@ -2,6 +2,9 @@ import { ChangeDetectionStrategy, Component } from '@angular/core'; import { IconButtonTypes } from '../../buttons'; import { ErrorService } from '../error.service'; import { animate, state, style, transition, trigger } from '@angular/animations'; +import { fromEvent, merge, Observable } from 'rxjs'; +import { delay, map, mapTo } from 'rxjs/operators'; +import { shareLast } from '../../utils'; @Component({ selector: 'iqser-full-page-error', @@ -9,20 +12,27 @@ import { animate, state, style, transition, trigger } from '@angular/animations' 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')), + state('offline', style({ top: '100px' })), + state('online', style({ top: '-100px' })), + transition('* => offline', animate('1s ease-out')), + transition('* => online', animate('3s ease-in')), ]), ], changeDetection: ChangeDetectionStrategy.OnPush, }) export class FullPageErrorComponent { readonly iconButtonTypes = IconButtonTypes; + readonly connectionStatus$: Observable; - constructor(readonly errorService: ErrorService) {} + constructor(readonly errorService: ErrorService) { + const offline$ = merge(fromEvent(window, 'offline'), this.errorService.badGatewayError$); + const online$ = fromEvent(window, 'online').pipe(shareLast()); + const removeIndicator$ = online$.pipe(delay(3000), mapTo(undefined)); + + this.connectionStatus$ = merge(online$, offline$, removeIndicator$).pipe(map(event => event?.type)); + } reload(): void { window.location.reload(); } - } diff --git a/src/lib/error/server-error-interceptor.ts b/src/lib/error/server-error-interceptor.ts index 2c359a4..af6bbf4 100644 --- a/src/lib/error/server-error-interceptor.ts +++ b/src/lib/error/server-error-interceptor.ts @@ -1,4 +1,4 @@ -import { HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http'; +import { HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HttpStatusCode } from '@angular/common/http'; import { Inject, Injectable, Optional } from '@angular/core'; import { MonoTypeOperatorFunction, Observable, throwError, timer } from 'rxjs'; import { catchError, mergeMap, retryWhen, tap } from 'rxjs/operators'; @@ -20,13 +20,13 @@ function backoffOnServerError(maxRetries = 3): MonoTypeOperatorFunction (seconds = updateSeconds(seconds))), mergeMap((error: HttpErrorResponse, index) => { - if ((error.status < 500 && error.status !== 0) || index === maxRetries) { + if ((error.status < HttpStatusCode.InternalServerError && 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); } + + console.error('An error occurred: ', error); + console.error(`Retrying in ${seconds} seconds...`); + return timer(seconds * 1000); }), ), ); @@ -38,18 +38,17 @@ export class ServerErrorInterceptor implements HttpInterceptor { private readonly _errorService: ErrorService, private readonly _keycloakService: KeycloakService, @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) => { // token expired - if (error.status === 403) { + if (error.status === HttpStatusCode.Forbidden) { this._keycloakService.logout(); } // server error - if (error.status >= 500 || error.status === 0) { + if (error.status >= HttpStatusCode.InternalServerError) { this._errorService.set(error); } return throwError(error);