Initialisierung gibt ein Feedback an den Benutzer aus. Feedback wenn Benutzer offline ist.

This commit is contained in:
Lorenz Hilpert
2024-09-13 16:05:54 +02:00
parent c3d9274766
commit e0cb0974cf
9 changed files with 170 additions and 42 deletions

View File

@@ -1 +1,21 @@
@if ($offlineBannerVisible()) {
<div [@fadeInOut] class="bg-brand text-white text-center fixed inset-x-0 top-0 z-tooltip p-4">
<h3 class="font-bold grid grid-flow-col items-center justify-center text-xl gap-4">
<div>
<ng-icon name="matWifiOff"></ng-icon>
</div>
<div>Sie sind offline, keine Verbindung zum Netzwerk.</div>
</h3>
<p>Bereits geladene Ihnalte werden angezeigt, Interaktionen sind aktuell nicht möglich.</p>
<button class="fixed top-5 right-4 text-3xl w-12 h-12" type="button" (click)="dismissOfflineBanner()">
<ng-icon name="matClose"></ng-icon>
</button>
</div>
}
@if (!$online()) {
<div [@fadeInOut] class="bg-brand fixed inset-x-0 top-0 z-tooltip h-1 animate-pulse"></div>
}
<router-outlet></router-outlet>

View File

@@ -1,5 +1,5 @@
import { DOCUMENT } from '@angular/common';
import { Component, HostListener, Inject, OnInit, Renderer2 } from '@angular/core';
import { Component, effect, HostListener, Inject, OnInit, Renderer2, signal, untracked } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { SwUpdate } from '@angular/service-worker';
import { ApplicationService } from '@core/application';
@@ -12,13 +12,41 @@ import { IsaLogProvider } from './providers';
import { EnvironmentService } from '@core/environment';
import { AuthService } from '@core/auth';
import { UiMessageModalComponent, UiModalService } from '@ui/modal';
import { injectOnline$ } from './services/network-status.service';
import { toSignal } from '@angular/core/rxjs-interop';
import { animate, style, transition, trigger } from '@angular/animations';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
animations: [
trigger('fadeInOut', [
transition(':enter', [
// :enter wird ausgelöst, wenn das Element zum DOM hinzugefügt wird
style({ opacity: 0, transform: 'translateY(-100%)' }),
animate('300ms', style({ opacity: 1, transform: 'translateY(0)' })),
]),
transition(':leave', [
// :leave wird ausgelöst, wenn das Element aus dem DOM entfernt wird
animate('300ms', style({ opacity: 0, transform: 'translateY(-100%)' })),
]),
]),
],
})
export class AppComponent implements OnInit {
$online = toSignal(injectOnline$());
$offlineBannerVisible = signal(false);
onlineEffect = effect(() => {
const online = this.$online();
untracked(() => {
this.$offlineBannerVisible.set(!online);
});
});
private _checkForUpdates: number = this._config.get('checkForUpdates');
get checkForUpdates(): number {
@@ -148,4 +176,8 @@ export class AppComponent implements OnInit {
});
}
}
dismissOfflineBanner() {
this.$offlineBannerVisible.set(false);
}
}

View File

@@ -37,6 +37,8 @@ import { NativeContainerService } from 'native-container';
import { ShellModule } from '@shared/shell';
import { MainComponent } from './main.component';
import { IconModule } from '@shared/components/icon';
import { NgIconsModule } from '@ng-icons/core';
import { matClose, matWifiOff } from '@ng-icons/material-icons/baseline';
registerLocaleData(localeDe, localeDeExtra);
registerLocaleData(localeDe, 'de', localeDeExtra);
@@ -46,26 +48,36 @@ export function _appInitializerFactory(
auth: AuthService,
injector: Injector,
scanAdapter: ScanAdapterService,
nativeContainer: NativeContainerService
nativeContainer: NativeContainerService,
) {
return async () => {
const statusElement = document.querySelector('#init-status');
statusElement.innerHTML = 'Konfigurationen werden geladen...';
await config.init();
statusElement.innerHTML = 'Authentifizierung wird geprüft...';
await auth.init();
const laoderElement = document.querySelector('#init-loader');
try {
statusElement.innerHTML = 'Konfigurationen werden geladen...';
await config.init();
statusElement.innerHTML = 'Authentifizierung wird geprüft...';
await auth.init();
if (auth.isAuthenticated()) {
statusElement.innerHTML = 'App wird initialisiert...';
const state = injector.get(RootStateService);
await state.init();
if (auth.isAuthenticated()) {
statusElement.innerHTML = 'App wird initialisiert...';
const state = injector.get(RootStateService);
await state.init();
}
statusElement.innerHTML = 'Native Container wird initialisiert...';
await nativeContainer.init();
statusElement.innerHTML = 'Scanner wird initialisiert...';
await scanAdapter.init();
} catch (error) {
laoderElement.remove();
statusElement.innerHTML =
'<b>Fehler bei der Initialisierung.</b><br><br>Bitte versuchen Sie es erneut. oder wenden Sie sich an den Support.<br><br>' + error;
console.error('Error during app initialization', error);
throw error;
}
statusElement.innerHTML = 'Native Container wird initialisiert...';
await nativeContainer.init();
statusElement.innerHTML = 'Scanner wird initialisiert...';
await scanAdapter.init();
};
}
@@ -107,6 +119,7 @@ export function _notificationsHubOptionsFactory(config: Config, auth: AuthServic
ScanditScanAdapterModule.forRoot(),
PlatformModule,
IconModule.forRoot(),
NgIconsModule.withIcons({ matWifiOff, matClose }),
],
providers: [
{

View File

@@ -2,30 +2,34 @@ import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpEvent, HttpHandler, HttpRequest, HttpErrorResponse } from '@angular/common/http';
import { NEVER, Observable, throwError } from 'rxjs';
import { UiMessageModalComponent, UiModalService } from '@ui/modal';
import { catchError, mergeMap, tap } from 'rxjs/operators';
import { catchError, filter, mergeMap, takeUntil, tap } from 'rxjs/operators';
import { AuthService } from '@core/auth';
import { IsaLogProvider } from '../providers';
import { LogLevel } from '@core/logger';
import { injectOnline$ } from '../services/network-status.service';
@Injectable()
export class HttpErrorInterceptor implements HttpInterceptor {
constructor(private _modal: UiModalService, private _auth: AuthService, private _isaLogProvider: IsaLogProvider) {}
readonly offline$ = injectOnline$().pipe(
filter((online) => !online),
tap(() => console.log('offline')),
);
constructor(
private _modal: UiModalService,
private _auth: AuthService,
private _isaLogProvider: IsaLogProvider,
) {}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(req).pipe(catchError((error: HttpErrorResponse, caught: any) => this.handleError(error)));
return next.handle(req).pipe(
takeUntil(this.offline$),
catchError((error: HttpErrorResponse, caught: any) => this.handleError(error)),
);
}
handleError(error: HttpErrorResponse): Observable<any> {
if (error.status === 0) {
return this._modal
.open({
content: UiMessageModalComponent,
title: 'Sie sind offline, keine Verbindung zum Netzwerk',
data: { message: 'Bereits geladene Inhalte werden angezeigt. Interaktionen sind aktuell nicht möglich.' },
})
.afterClosed$.pipe(mergeMap(() => throwError(error)));
} else if (error.status === 401) {
console.log('401', error);
if (error.status === 401) {
return this._modal
.open({
content: UiMessageModalComponent,
@@ -36,11 +40,13 @@ export class HttpErrorInterceptor implements HttpInterceptor {
tap(() => {
this._auth.login();
}),
mergeMap(() => NEVER)
mergeMap(() => NEVER),
);
}
this._isaLogProvider.log(LogLevel.ERROR, 'Http Error', error);
if (!error.url.endsWith('/isa/logging')) {
this._isaLogProvider.log(LogLevel.ERROR, 'Http Error', error);
}
return throwError(error);
}

View File

@@ -7,7 +7,11 @@ import { LogLevel } from '@core/logger';
@Injectable({ providedIn: 'root' })
export class IsaErrorHandler implements ErrorHandler {
constructor(private _modal: UiModalService, private _authService: AuthService, private _isaLogProvider: IsaLogProvider) {}
constructor(
private _modal: UiModalService,
private _authService: AuthService,
private _isaLogProvider: IsaLogProvider,
) {}
async handleError(error: any): Promise<void> {
console.error(error);
@@ -37,13 +41,13 @@ export class IsaErrorHandler implements ErrorHandler {
this._isaLogProvider.log(LogLevel.ERROR, 'Client Error', error);
this._modal.open({
content: UiErrorModalComponent,
title:
!navigator.onLine || (error instanceof HttpErrorResponse && error?.status === 0)
? 'Sie sind offline, keine Verbindung zum Netzwerk'
: 'Unbekannter Fehler',
data: error,
});
// this._modal.open({
// content: UiErrorModalComponent,
// title:
// !navigator.onLine || (error instanceof HttpErrorResponse && error?.status === 0)
// ? 'Sie sind offline, keine Verbindung zum Netzwerk'
// : 'Unbekannter Fehler',
// data: error,
// });
}
}

View File

@@ -0,0 +1,17 @@
import { inject, Injectable } from '@angular/core';
import { fromEvent, map, merge, of } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class NetworkStatusService {
online$ = merge(
of(navigator.onLine),
fromEvent(window, 'online').pipe(map(() => true)),
fromEvent(window, 'offline').pipe(map(() => false)),
);
status$ = this.online$.pipe(map((online) => (online ? 'online' : 'offline')));
}
export const injectNetworkStatus$ = () => inject(NetworkStatusService).status$;
export const injectOnline$ = () => inject(NetworkStatusService).online$;

View File

@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
@@ -15,7 +15,7 @@
<app-root>
<div class="grid place-items-center h-screen">
<div class="grid grid-flow-row gap-4 items-center justify-center">
<div class="flex flex-col items-center">
<div id="init-loader" class="flex flex-col items-center">
<img class="animate-spin" src="/assets/images/spinner.svg" alt="spinner animation" />
</div>
<div id="init-status" class="text-center">App wird geladen</div>

34
package-lock.json generated
View File

@@ -20,6 +20,8 @@
"@angular/router": "^17.3.10",
"@angular/service-worker": "^17.3.10",
"@microsoft/signalr": "^7.0.0",
"@ng-icons/core": "^27.5.2",
"@ng-icons/material-icons": "^29.5.0",
"@ngrx/component-store": "^17.2.0",
"@ngrx/effects": "^17.2.0",
"@ngrx/entity": "^17.2.0",
@@ -3403,6 +3405,22 @@
"ws": "^7.4.5"
}
},
"node_modules/@ng-icons/core": {
"version": "27.5.2",
"resolved": "https://registry.npmjs.org/@ng-icons/core/-/core-27.5.2.tgz",
"integrity": "sha512-LDfhrfxJ5yM8NkpSlz9ix+RRuecFHZYjfCgXH0dVDuk5gx+40Dxi/v9vL8oRpUP4oL9spR7Ndry2ENVZY/h4Tw==",
"dependencies": {
"tslib": "^2.2.0"
}
},
"node_modules/@ng-icons/material-icons": {
"version": "29.5.0",
"resolved": "https://registry.npmjs.org/@ng-icons/material-icons/-/material-icons-29.5.0.tgz",
"integrity": "sha512-iPkyDJ/fy9i4m4DUp2krHjMiG9XOiYozonHKJg5O/RlzjsQE4+aTyp+Imm8VAXDC35KgTO0aeeo6rltiGypWqg==",
"dependencies": {
"tslib": "^2.2.0"
}
},
"node_modules/@ngneat/spectator": {
"version": "15.0.1",
"resolved": "https://registry.npmjs.org/@ngneat/spectator/-/spectator-15.0.1.tgz",
@@ -18342,6 +18360,22 @@
"ws": "^7.4.5"
}
},
"@ng-icons/core": {
"version": "27.5.2",
"resolved": "https://registry.npmjs.org/@ng-icons/core/-/core-27.5.2.tgz",
"integrity": "sha512-LDfhrfxJ5yM8NkpSlz9ix+RRuecFHZYjfCgXH0dVDuk5gx+40Dxi/v9vL8oRpUP4oL9spR7Ndry2ENVZY/h4Tw==",
"requires": {
"tslib": "^2.2.0"
}
},
"@ng-icons/material-icons": {
"version": "29.5.0",
"resolved": "https://registry.npmjs.org/@ng-icons/material-icons/-/material-icons-29.5.0.tgz",
"integrity": "sha512-iPkyDJ/fy9i4m4DUp2krHjMiG9XOiYozonHKJg5O/RlzjsQE4+aTyp+Imm8VAXDC35KgTO0aeeo6rltiGypWqg==",
"requires": {
"tslib": "^2.2.0"
}
},
"@ngneat/spectator": {
"version": "15.0.1",
"resolved": "https://registry.npmjs.org/@ngneat/spectator/-/spectator-15.0.1.tgz",

View File

@@ -71,6 +71,8 @@
"@angular/router": "^17.3.10",
"@angular/service-worker": "^17.3.10",
"@microsoft/signalr": "^7.0.0",
"@ng-icons/core": "^27.5.2",
"@ng-icons/material-icons": "^29.5.0",
"@ngrx/component-store": "^17.2.0",
"@ngrx/effects": "^17.2.0",
"@ngrx/entity": "^17.2.0",