change offline indicator design & improve offline state detection

This commit is contained in:
Dan Percic 2021-11-10 00:54:07 +02:00
parent beae619691
commit ca957326b3
5 changed files with 77 additions and 67 deletions

View File

@ -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<HttpErrorResponse | undefined>;
private readonly _errorEvent$ = new BehaviorSubject<HttpErrorResponse | undefined>(undefined);
readonly error$ = new BehaviorSubject<HttpErrorResponse | undefined>(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);
}
}

View File

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

View File

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

View File

@ -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<string | undefined>;
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();
}
}

View File

@ -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<HttpEven
attempts.pipe(
tap(() => (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<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
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);