Merge branch 'master' into VM/RED-2614

This commit is contained in:
Valentin 2022-01-13 12:06:02 +02:00
commit f7004520a7
18 changed files with 171 additions and 84 deletions

View File

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Svg Vector Icons : http://www.onlinewebfonts.com/icon -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 1000 1000" enable-background="new 0 0 1000 1000" xml:space="preserve">
<metadata> Svg Vector Icons : http://www.onlinewebfonts.com/icon </metadata>
<g><g transform="translate(0.000000,511.000000) scale(0.100000,-0.100000)"><path d="M4359.4,4986.1c-588-84.9-1067.8-233.5-1581.5-494.6c-955.3-484-1713.1-1252.5-2186.5-2218.3c-225-460.7-352.4-853.4-450-1384.1c-55.2-305.7-55.2-1244,0-1549.7c97.7-530.7,225-923.4,450-1384.1C1202.8-3290.8,2308.8-4220.5,3644-4606.9c849.1-246.2,1855.3-246.2,2704.5,0c1311.9,380,2409.4,1290.7,3027.1,2509.2c360.9,711.1,524.3,1401.1,524.3,2212c0,813-163.5,1498.7-526.4,2218.3c-481.9,953.2-1254.6,1715.2-2218.4,2186.5c-454.3,222.9-819.4,341.8-1331,439.4C5529.1,5013.7,4663,5030.7,4359.4,4986.1z M5270.1,4232.5C6280.6,4162.5,7197.7,3746.4,7913,3031c628.4-628.4,1016.8-1390.4,1161.2-2279.9c57.3-339.6,59.5-904.3,8.5-1231.2c-286.6-1827.7-1664.3-3205.5-3492-3492c-326.9-51-891.6-48.8-1231.2,8.5c-1358.6,222.9-2481.6,1055-3080.2,2284.2C515-106.5,835.6,1787,2079.5,3031c694.1,694.2,1638.8,1131.5,2592,1199.4c125.3,10.6,244.1,19.1,261.1,19.1C4949.6,4251.6,5102.4,4243.1,5270.1,4232.5z"/><path d="M5293.5,2922.7c-193.2-65.8-358.7-227.1-420.3-407.6c-14.9-40.3-25.5-135.9-25.5-214.4c0-554.1,728.1-834.3,1154.8-443.7c152.8,140.1,210.1,263.2,212.3,454.3c2.1,188.9-72.2,352.4-214.4,475.5C5819.9,2948.2,5524.9,3003.4,5293.5,2922.7z"/><path d="M4688.5,1075.9c-125.2-17-322.7-74.3-577.4-169.8c-254.7-93.4-261.1-97.6-282.3-171.9c-29.7-104-50.9-237.8-38.2-237.8c6.4,0,65.8,19.1,135.9,44.6c155,53.1,411.8,59.4,526.5,10.6c97.6-40.3,140.1-133.8,140.1-309.9c0-178.3-40.3-356.6-231.4-1035.9c-191.1-668.7-222.9-842.8-210.2-1091.1c6.4-144.3,21.2-197.4,70-288.7c116.8-216.5,316.3-343.9,605-388.5c305.7-46.7,592.3,6.4,1072,199.5l169.8,67.9l38.2,144.3c21.2,80.7,36.1,150.7,31.8,155c-4.2,4.2-70-12.7-144.3-36.1c-161.3-51-486.1-48.8-568.9,2.1c-72.2,48.8-110.4,150.7-110.4,303.6c0,186.8,36.1,343.9,233.5,1044.4c178.3,630.5,222.9,864,208,1091.1c-25.4,348.1-254.7,590.1-624.1,658.1C5013.3,1090.8,4818,1092.9,4688.5,1075.9z"/></g></g>
</svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -28,6 +28,7 @@
--iqser-green-2: #5ce594;
--iqser-yellow-1: #ffb83b;
--iqser-yellow-2: #fdbd00;
--iqser-yellow-rgb: 253, 189, 0;
--iqser-red-1: #dd4d50;
--iqser-blue-5: #c5d3eb;
--iqser-helpmode-primary: green;

View File

@ -76,7 +76,7 @@
display: flex;
justify-content: center;
align-items: center;
width: fit-content;
min-width: fit-content;
mat-icon {
width: 10px;
@ -88,6 +88,21 @@
&:not(:last-child) {
margin-right: 12px;
}
&.warn, &.error {
opacity: 1;
padding: 3px 4px;
border-radius: 3px;
}
&.warn {
background-color: rgba(var(--iqser-yellow-rgb), 0.7);
}
&.error {
background-color: rgba(var(--iqser-primary-rgb), 0.1);
color: var(--iqser-primary);
}
}
&:not(.cell) {

View File

@ -14,7 +14,7 @@ import {
ToastComponent,
} from './misc';
import { FullPageLoadingIndicatorComponent } from './loading';
import { FullPageErrorComponent } from './error';
import { ConnectionStatusComponent, FullPageErrorComponent } from './error';
import { IqserListingModule } from './listing';
import { IqserFiltersModule } from './filtering';
import { IqserInputsModule } from './inputs';
@ -45,6 +45,7 @@ const modules = [
const components = [
StatusBarComponent,
FullPageLoadingIndicatorComponent,
ConnectionStatusComponent,
FullPageErrorComponent,
LogoComponent,
HiddenActionComponent,

View File

@ -0,0 +1,8 @@
<div
*ngIf="errorService.connectionStatus$ | async as status"
[@animateOpenClose]="status"
[ngClass]="status"
class="indicator flex-align-items-center"
>
<span [translate]="connectionStatusTranslations[status]"></span>
</div>

View File

@ -0,0 +1,32 @@
@use 'sass:math';
.indicator {
$width: 160px;
position: fixed;
top: 0;
right: 50%;
left: 50%;
height: 40px;
margin-left: -(math.div($width, 2));
width: $width;
border-radius: 8px;
z-index: 5;
span {
color: var(--iqser-grey-1);
text-align: center;
line-height: 18px;
font-weight: 600;
height: 18px;
width: 100%;
}
}
.offline {
background: var(--iqser-yellow-2);
}
.online {
background: var(--iqser-green-2);
}

View File

@ -0,0 +1,24 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { connectionStatusTranslations } from '../../translations/connection-status-translations';
import { animate, state, style, transition, trigger } from '@angular/animations';
import { ErrorService } from '../error.service';
@Component({
selector: 'iqser-connection-status',
templateUrl: './connection-status.component.html',
styleUrls: ['./connection-status.component.scss'],
animations: [
trigger('animateOpenClose', [
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 ConnectionStatusComponent {
connectionStatusTranslations = connectionStatusTranslations;
constructor(readonly errorService: ErrorService) {}
}

View File

@ -6,6 +6,24 @@ import { delay, filter, map, mapTo } from 'rxjs/operators';
import { NavigationStart, Router } from '@angular/router';
import { shareLast } from '../utils';
export class CustomError {
readonly label: string;
readonly actionLabel: string;
readonly actionIcon: string;
readonly message?: string;
readonly action?: () => void;
constructor(label: string, actionLabel: string, actionIcon: string, message?: string, action?: () => void) {
this.label = label;
this.actionLabel = actionLabel;
this.actionIcon = actionIcon;
this.message = message;
this.action = action;
}
}
export type ErrorType = HttpErrorResponse | CustomError | undefined;
const BANDWIDTH_LIMIT_EXCEEDED = 509 as const;
const OFFLINE_STATUSES = [
HttpStatusCode.BadGateway,
@ -14,15 +32,15 @@ const OFFLINE_STATUSES = [
BANDWIDTH_LIMIT_EXCEEDED,
] as const;
const isOffline = (error?: HttpErrorResponse) => !!error && OFFLINE_STATUSES.includes(error.status);
const isOffline = (error?: ErrorType) => error instanceof HttpErrorResponse && OFFLINE_STATUSES.includes(error.status);
@Injectable({ providedIn: 'root' })
export class ErrorService {
readonly error$ = new Subject<HttpErrorResponse | undefined>();
readonly offline$: Observable<Event>;
readonly online$: Observable<Event>;
readonly serverError$ = this.error$.pipe(filter(error => !error || !isOffline(error)));
readonly connectionStatus$: Observable<string | undefined>;
private readonly _error$ = new Subject<ErrorType>();
readonly error$ = this._error$.pipe(filter(error => !error || !isOffline(error)));
private readonly _online$ = new Subject();
constructor(private readonly _loadingService: LoadingService, private readonly _router: Router) {
@ -36,9 +54,9 @@ export class ErrorService {
this.connectionStatus$ = merge(this.online$, this.offline$, removeIndicator$).pipe(map(event => event?.type));
}
set(error: HttpErrorResponse): void {
set(error: ErrorType): void {
this._loadingService.stop();
this.error$.next(error);
this._error$.next(error);
}
setOnline(): void {
@ -46,11 +64,11 @@ export class ErrorService {
}
clear(): void {
this.error$.next();
this._error$.next();
}
private _offline() {
return merge(fromEvent(window, 'offline'), this.error$.pipe(filter(isOffline), mapTo(new Event('offline')), shareLast()));
return merge(fromEvent(window, 'offline'), this._error$.pipe(filter(isOffline), mapTo(new Event('offline')), shareLast()));
}
private _online() {

View File

@ -1,28 +1,19 @@
<div
*ngIf="errorService.connectionStatus$ | async as status"
[@animateOpenClose]="status"
[ngClass]="status"
class="indicator flex-align-items-center"
>
<span [translate]="translations[status]"></span>
</div>
<ng-container *ngIf="errorService.serverError$ | async as error">
<ng-container *ngIf="errorService.error$ | async as error">
<section class="full-page-section"></section>
<section class="full-page-content flex-align-items-center">
<mat-icon svgIcon="iqser:failure"></mat-icon>
<div [translate]="'error.title'" class="heading-l mt-24"></div>
<div [translate]="errorTitle(error)" class="heading-l mt-24"></div>
<div class="mt-16 error">{{ error.message }}</div>
<div *ngIf="error.message" class="mt-16 error">{{ error.message }}</div>
<iqser-icon-button
(action)="reload()"
[label]="'error.reload' | translate"
(action)="action(error)"
[icon]="actionIcon(error)"
[label]="actionLabel(error) | translate"
[type]="iconButtonTypes.primary"
class="mt-20"
icon="iqser:refresh"
></iqser-icon-button>
</section>
</ng-container>

View File

@ -1,36 +1,3 @@
@use 'sass:math';
.indicator {
$width: 160px;
position: fixed;
top: 0;
right: 50%;
left: 50%;
height: 40px;
margin-left: -(math.div($width, 2));
width: $width;
border-radius: 8px;
z-index: 5;
span {
color: var(--iqser-grey-1);
text-align: center;
line-height: 18px;
font-weight: 600;
height: 18px;
width: 100%;
}
}
.offline {
background: var(--iqser-yellow-2);
}
.online {
background: var(--iqser-green-2);
}
.full-page-section {
opacity: 0.95;
}
@ -50,7 +17,7 @@
}
}
:is(.offline-box, .full-page-content) .heading-l {
color: #9398a0;
.heading-l {
color: var(--iqser-grey-7);
font-weight: initial;
}

View File

@ -1,30 +1,36 @@
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 { connectionStatusTranslations } from '../../translations/connection-status-translations';
import { CustomError, ErrorService, ErrorType } from '../error.service';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
@Component({
selector: 'iqser-full-page-error',
templateUrl: './full-page-error.component.html',
styleUrls: ['./full-page-error.component.scss'],
animations: [
trigger('animateOpenClose', [
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 {
translations = connectionStatusTranslations;
readonly iconButtonTypes = IconButtonTypes;
constructor(readonly errorService: ErrorService) {}
reload(): void {
window.location.reload();
errorTitle(error: ErrorType): string {
return error instanceof CustomError ? error.label : _('error.title');
}
actionLabel(error: ErrorType): string {
return error instanceof CustomError ? error.actionLabel : _('error.reload');
}
actionIcon(error: ErrorType): string {
return error instanceof CustomError ? error.actionIcon : 'iqser:refresh';
}
action(error: ErrorType): void {
if (error instanceof CustomError && error.action) {
error.action();
} else {
window.location.reload();
}
}
}

View File

@ -2,3 +2,4 @@ export * from './error.service';
export * from './max-retries.token';
export * from './server-error-interceptor';
export * from './full-page-error/full-page-error.component';
export * from './connection-status/connection-status.component';

View File

@ -19,7 +19,6 @@ export class IqserIconsModule {
'collapse',
'csv',
'document',
'dossier-info',
'download',
'edit',
'expand',

View File

@ -22,6 +22,7 @@ export class EntitiesService<E extends IListable, I = E> extends GenericService<
readonly all$: Observable<E[]>;
readonly allLength$: Observable<number>;
protected readonly _entityChanged$ = new Subject<E>();
protected readonly _entityDeleted$ = new Subject<E>();
private readonly _all$ = new BehaviorSubject<E[]>([]);
constructor(
@ -68,8 +69,13 @@ export class EntitiesService<E extends IListable, I = E> extends GenericService<
);
}
getEntityDeleted$(entityId: string): Observable<E | undefined> {
return this._entityDeleted$.pipe(filter(entity => entity.id === entityId));
}
setEntities(entities: E[]): void {
const changedEntities: E[] = [];
const deletedEntities = this.all.filter(oldEntity => !entities.find(newEntity => newEntity.id === oldEntity.id));
// Keep old object references for unchanged entities
const newEntities = entities.map(entity => {
@ -90,6 +96,10 @@ export class EntitiesService<E extends IListable, I = E> extends GenericService<
for (const entity of changedEntities) {
this._entityChanged$.next(entity);
}
for (const entity of deletedEntities) {
this._entityDeleted$.next(entity);
}
}
find(id: string): E | undefined {

View File

@ -23,7 +23,7 @@ export interface QueryParam {
*/
export abstract class GenericService<I> {
protected readonly _http = this._injector.get(HttpClient);
private readonly _lastCheckedForChanges = new Map<string, string>([[ROOT_CHANGES_KEY, '0']]);
protected readonly _lastCheckedForChanges = new Map<string, string>([[ROOT_CHANGES_KEY, '0']]);
protected constructor(protected readonly _injector: Injector, protected readonly _defaultModelPath: string) {}

View File

@ -4,6 +4,7 @@ import { HttpClient } from '@angular/common/http';
import { tap } from 'rxjs/operators';
import { HeadersConfiguration, mapEach, RequiredParam, Validate } from '../utils';
/* WIP, not used */
@Injectable()
export abstract class StatsService<E, I = E> {
private readonly _http = this._injector.get(HttpClient);

View File

@ -1,12 +1,14 @@
import { Directive, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs';
import { OnDetach } from './custom-route-reuse.strategy';
/**
* Inherit this class when you need to subscribe to observables in your components
*/
@Directive()
export abstract class AutoUnsubscribe implements OnDestroy {
export abstract class AutoUnsubscribe implements OnDestroy, OnDetach {
private readonly _subscriptions = new Subscription();
private readonly _activeScreenSubscriptions = new Subscription();
/**
* Call this method when you want to subscribe to an observable
@ -17,12 +19,29 @@ export abstract class AutoUnsubscribe implements OnDestroy {
this._subscriptions.add(subscription);
}
/**
* Call this method when you want to subscribe to an observable while the screen is active
* @param subscription - the new subscription to add to subscriptions array
*/
set addActiveScreenSubscription(subscription: Subscription) {
this._activeScreenSubscriptions.closed = false;
this._activeScreenSubscriptions.add(subscription);
}
/**
* This method unsubscribes active subscriptions
* If you implement OnDestroy in a component that inherits AutoUnsubscribeComponent,
* then you must explicitly call super.ngOnDestroy()
*/
ngOnDestroy(): void {
this.ngOnDetach();
this._subscriptions.unsubscribe();
}
/**
* This method unsubscribes active subscriptions for active screens
*/
ngOnDetach(): void {
this._activeScreenSubscriptions.unsubscribe();
}
}

View File

@ -1,6 +1,7 @@
import { tap } from 'rxjs/operators';
import { ITrackable } from '../listing/models/trackable';
import { MonoTypeOperatorFunction } from 'rxjs';
import moment from 'moment';
export function capitalize(value: string): string {
if (!value) {
@ -19,7 +20,7 @@ export function humanize(value: string, lowercase = true): string {
}
export function log<T>(): MonoTypeOperatorFunction<T> {
return tap<T>(res => console.log(res));
return tap<T>(res => console.log(`%c[${moment().format('HH:mm:ss.SSS')}]`, 'color: yellow;', res));
}
export function toNumber(str: string): number {