From 5f37743019c7e6807efa43dfcaaccf025bdc96f4 Mon Sep 17 00:00:00 2001 From: Marcel Arndt Date: Mon, 30 Jun 2025 07:52:27 +0200 Subject: [PATCH] feat: prepare skeleton loading indicator --- .../skeleton-rect.component.spec.ts | 23 ++++++++ .../skeleton-rect/skeleton-rect.component.ts | 57 +++++++++++++++++++ .../skeleton-rect/skeleton.directive.ts | 43 ++++++++++++++ .../core/telemetry/global-error-handler.ts | 3 +- .../src/app/pages/home/home.component.ts | 16 ++++-- .../manager-info-card.component.html | 3 + .../manager-info-card.component.ts | 8 ++- .../ticket-lane/ticket-lane.component.html | 41 ++++--------- .../ticket-lane/ticket-lane.component.ts | 4 +- .../ticket-state-info-card.component.html | 3 + .../ticket-state-info-card.component.ts | 8 ++- .../ticket-system/tickets-page.component.ts | 5 -- service/app-hub/dashboard/src/styles.scss | 19 ++++++- 13 files changed, 184 insertions(+), 49 deletions(-) create mode 100644 service/app-hub/dashboard/src/app/core/components/skeleton-rect/skeleton-rect.component.spec.ts create mode 100644 service/app-hub/dashboard/src/app/core/components/skeleton-rect/skeleton-rect.component.ts create mode 100644 service/app-hub/dashboard/src/app/core/components/skeleton-rect/skeleton.directive.ts diff --git a/service/app-hub/dashboard/src/app/core/components/skeleton-rect/skeleton-rect.component.spec.ts b/service/app-hub/dashboard/src/app/core/components/skeleton-rect/skeleton-rect.component.spec.ts new file mode 100644 index 0000000..a84a56b --- /dev/null +++ b/service/app-hub/dashboard/src/app/core/components/skeleton-rect/skeleton-rect.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SkeletonRectComponent } from './skeleton-rect.component'; + +describe('SkeletonRectComponentComponent', () => { + let component: SkeletonRectComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [SkeletonRectComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(SkeletonRectComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/service/app-hub/dashboard/src/app/core/components/skeleton-rect/skeleton-rect.component.ts b/service/app-hub/dashboard/src/app/core/components/skeleton-rect/skeleton-rect.component.ts new file mode 100644 index 0000000..5a9f8a1 --- /dev/null +++ b/service/app-hub/dashboard/src/app/core/components/skeleton-rect/skeleton-rect.component.ts @@ -0,0 +1,57 @@ +import { Component, ElementRef } from '@angular/core'; + +@Component({ + selector: 'app-skeleton-rect', + imports: [], + host: { + class: 'pulse', + }, + template: `
+ + Loading... +
`, + styles: [ + ` + :host { + /* display: block; + width: var(--skeleton-rect-width); + height: var(--skeleton-rect-height); + background: rgb(239, 241, 246) no-repeat; */ + } + `, + ], +}) +export class SkeletonRectComponent { + width?: string; + height?: string; + className?: string; + + constructor(private host: ElementRef) {} + + ngOnInit() { + const host = this.host.nativeElement; + + if (this.className) { + host.classList.add(this.className); + } + + host.style.setProperty('--skeleton-rect-width', this.width ?? '100%'); + host.style.setProperty('--skeleton-rect-height', this.height ?? '20px'); + } +} diff --git a/service/app-hub/dashboard/src/app/core/components/skeleton-rect/skeleton.directive.ts b/service/app-hub/dashboard/src/app/core/components/skeleton-rect/skeleton.directive.ts new file mode 100644 index 0000000..9bbb836 --- /dev/null +++ b/service/app-hub/dashboard/src/app/core/components/skeleton-rect/skeleton.directive.ts @@ -0,0 +1,43 @@ +import { + Directive, + Input, + TemplateRef, + ViewContainerRef, + SimpleChanges, +} from '@angular/core'; +import { SkeletonRectComponent } from './skeleton-rect.component'; + +function random(min: number, max: number): number { + return Math.floor(Math.random() * (max - min + 1)) + min; +} + +@Directive({ selector: '[skeleton]' }) +export class SkeletonDirective { + @Input('skeleton') isLoading = false; + @Input('skeletonRepeat') size = 1; + @Input('skeletonWidth') width?: string; + @Input('skeletonHeight') height?: string; + @Input('skeletonClassName') className?: string; + + constructor(private tpl: TemplateRef, private vcr: ViewContainerRef) {} + + ngOnChanges(changes: SimpleChanges) { + if (changes['isLoading']) { + this.vcr.clear(); + + if (changes['isLoading'].currentValue) { + Array.from({ length: this.size }).forEach(() => { + const ref = this.vcr.createComponent(SkeletonRectComponent); + + Object.assign(ref.instance, { + width: this.width === 'rand' ? `${random(30, 90)}%` : this.width, + height: this.height, + className: this.className, + }); + }); + } else { + this.vcr.createEmbeddedView(this.tpl); + } + } + } +} diff --git a/service/app-hub/dashboard/src/app/core/telemetry/global-error-handler.ts b/service/app-hub/dashboard/src/app/core/telemetry/global-error-handler.ts index ca870df..5284211 100644 --- a/service/app-hub/dashboard/src/app/core/telemetry/global-error-handler.ts +++ b/service/app-hub/dashboard/src/app/core/telemetry/global-error-handler.ts @@ -1,11 +1,10 @@ import { ErrorHandler, Injectable } from '@angular/core'; -import { faro } from '@grafana/faro-web-sdk'; @Injectable() export class GlobalErrorHandler implements ErrorHandler { handleError(error: any) { if (error instanceof Error) { - faro.api.pushError(error); + // faro.api.pushError(error); } console.error(error); } diff --git a/service/app-hub/dashboard/src/app/pages/home/home.component.ts b/service/app-hub/dashboard/src/app/pages/home/home.component.ts index 85d86d2..30b57dd 100644 --- a/service/app-hub/dashboard/src/app/pages/home/home.component.ts +++ b/service/app-hub/dashboard/src/app/pages/home/home.component.ts @@ -11,6 +11,7 @@ import { import { ManagerInfoCardComponent } from '../manager/components/manager-info-card/manager-info-card.component'; import { TicketStateInfoCardComponent } from '../ticket-system/ticket-state-info-card/ticket-state-info-card.component'; import { LinkCardComponent } from './link-card/link-card.component'; +import { SkeletonDirective } from '../../core/components/skeleton-rect/skeleton.directive'; @Component({ selector: 'app-home', @@ -21,6 +22,7 @@ import { LinkCardComponent } from './link-card/link-card.component'; LinkCardComponent, MatDividerModule, CurrencyPipe, + SkeletonDirective, ], template: `
@@ -196,12 +198,14 @@ export class HomeComponent implements OnInit { }; }, {} as Record); - return Object.entries(groupedByOrdinanceType).map(([ot, revenues]) => { - return { - label: ot, - value: revenues.at(-1), //.reduce((sum, cur) => (sum += cur), 0), - }; - }); + return Object.entries(groupedByOrdinanceType) + .sort(([ot1], [ot2]) => ot1.localeCompare(ot2)) + .map(([ot, revenues]) => { + return { + label: ot, + value: revenues.at(-1), //.reduce((sum, cur) => (sum += cur), 0), + }; + }); }); constructor(private readonly homeDashboardGql: HomeDashboardGQL) { diff --git a/service/app-hub/dashboard/src/app/pages/manager/components/manager-info-card/manager-info-card.component.html b/service/app-hub/dashboard/src/app/pages/manager/components/manager-info-card/manager-info-card.component.html index 3cc3c81..6516aad 100644 --- a/service/app-hub/dashboard/src/app/pages/manager/components/manager-info-card/manager-info-card.component.html +++ b/service/app-hub/dashboard/src/app/pages/manager/components/manager-info-card/manager-info-card.component.html @@ -1,3 +1,6 @@ + @if (kpi()?.main?.label) { (false); ordinanceType = input.required(); kpi = signal(undefined); metrics = signal([]); constructor() { effect(() => { + this.isLoading.set(true); this.service .fetchManagerKpi(this.ordinanceType()) .pipe( tap((res) => this.metrics.set(res.additional)), - tap((res) => this.kpi.set(res)) + tap((res) => this.kpi.set(res)), + tap(() => this.isLoading.set(false)) ) .subscribe(); }); diff --git a/service/app-hub/dashboard/src/app/pages/ticket-system/ticket-lane/ticket-lane.component.html b/service/app-hub/dashboard/src/app/pages/ticket-system/ticket-lane/ticket-lane.component.html index 534b0e2..ed45f90 100644 --- a/service/app-hub/dashboard/src/app/pages/ticket-system/ticket-lane/ticket-lane.component.html +++ b/service/app-hub/dashboard/src/app/pages/ticket-system/ticket-lane/ticket-lane.component.html @@ -4,33 +4,16 @@ (filterChange)="filterChanged($event)" > @if (tickets$ | async; as tickets) { - - - - - + + + + } diff --git a/service/app-hub/dashboard/src/app/pages/ticket-system/ticket-lane/ticket-lane.component.ts b/service/app-hub/dashboard/src/app/pages/ticket-system/ticket-lane/ticket-lane.component.ts index ad85800..3998160 100644 --- a/service/app-hub/dashboard/src/app/pages/ticket-system/ticket-lane/ticket-lane.component.ts +++ b/service/app-hub/dashboard/src/app/pages/ticket-system/ticket-lane/ticket-lane.component.ts @@ -52,8 +52,8 @@ import { TicketLaneHeaderComponent } from './ticket-lane-header.component'; TicketLaneHeaderComponent, TicketItemComponent, AsyncPipe, - ScrollingModule -], + ScrollingModule, + ], }) export class TicketLaneComponent { private dialog = inject(DialogService); diff --git a/service/app-hub/dashboard/src/app/pages/ticket-system/ticket-state-info-card/ticket-state-info-card.component.html b/service/app-hub/dashboard/src/app/pages/ticket-system/ticket-state-info-card/ticket-state-info-card.component.html index e7718e6..f8759ff 100644 --- a/service/app-hub/dashboard/src/app/pages/ticket-system/ticket-state-info-card/ticket-state-info-card.component.html +++ b/service/app-hub/dashboard/src/app/pages/ticket-system/ticket-state-info-card/ticket-state-info-card.component.html @@ -1,3 +1,6 @@ + @if (kpi()?.main?.label) { (false); ticketState = input.required(); kpi = signal(undefined); metrics = signal([]); constructor() { effect(() => { + this.isLoading.set(true); this.service .fetchTicketStateKpi(this.ticketState()) .pipe( tap((res) => this.metrics.set(res.additional)), - tap((res) => this.kpi.set(res)) + tap((res) => this.kpi.set(res)), + tap(() => this.isLoading.set(false)) ) .subscribe(); }); diff --git a/service/app-hub/dashboard/src/app/pages/ticket-system/tickets-page.component.ts b/service/app-hub/dashboard/src/app/pages/ticket-system/tickets-page.component.ts index 4919055..be2ee5a 100644 --- a/service/app-hub/dashboard/src/app/pages/ticket-system/tickets-page.component.ts +++ b/service/app-hub/dashboard/src/app/pages/ticket-system/tickets-page.component.ts @@ -44,11 +44,6 @@ import { TicketsSearchDialogComponent } from './tickets-search-dialog/tickets-se class="overflow-y-auto mat-elevation-z2" [ticketState]="TicketState.DocumentsMissing" > - `, styles: [ ` diff --git a/service/app-hub/dashboard/src/styles.scss b/service/app-hub/dashboard/src/styles.scss index b0b49ac..50046d0 100644 --- a/service/app-hub/dashboard/src/styles.scss +++ b/service/app-hub/dashboard/src/styles.scss @@ -1,5 +1,5 @@ /* You can add global styles to this file, and also import other style files */ -@use '@angular/material' as mat; +@use "@angular/material" as mat; html, body { @@ -11,3 +11,20 @@ body { background: var(--mat-sys-surface-dim); color: var(--mat-sys-on-surface); } + +.pulse { + animation: pulse 1.5s cubic-bezier(0.4, 0, 0.2, 1) infinite; + animation-delay: 0.5s; +} + +@keyframes pulse { + 0% { + opacity: 1; + } + 50% { + opacity: 0.5; + } + 100% { + opacity: 1; + } +}