change offline indicator design & improve offline state detection
This commit is contained in:
parent
beae619691
commit
ca957326b3
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user