diff --git a/projects/assets-library/assets/images/loader-expandable-row.gif b/projects/assets-library/assets/images/loader-expandable-row.gif new file mode 100644 index 000000000..385eef56c Binary files /dev/null and b/projects/assets-library/assets/images/loader-expandable-row.gif differ diff --git a/projects/assets-library/assets/images/loader-page.gif b/projects/assets-library/assets/images/loader-page.gif new file mode 100644 index 000000000..eaa47e252 Binary files /dev/null and b/projects/assets-library/assets/images/loader-page.gif differ diff --git a/projects/assets-library/assets/images/loader-spinner.gif b/projects/assets-library/assets/images/loader-spinner.gif new file mode 100644 index 000000000..21c09945a Binary files /dev/null and b/projects/assets-library/assets/images/loader-spinner.gif differ diff --git a/projects/assets-library/src/images/image-type.ts b/projects/assets-library/src/images/image-type.ts new file mode 100644 index 000000000..bd2b6f98a --- /dev/null +++ b/projects/assets-library/src/images/image-type.ts @@ -0,0 +1,12 @@ +export const enum ImagesAssetPath { + ErrorPage = 'assets/images/error-page.svg', + LoaderSpinner = 'assets/images/loader-spinner.gif', + LoaderPage = 'assets/images/loader-page.gif', + LoaderExpandableRow = 'assets/images/loader-expandable-row.gif' +} + +export const enum LoaderType { + Spinner = 'spinner', + ExpandableRow = 'expandable-row', + Page = 'page' +} diff --git a/projects/assets-library/src/public-api.ts b/projects/assets-library/src/public-api.ts index 1a6918a79..f42929a4f 100644 --- a/projects/assets-library/src/public-api.ts +++ b/projects/assets-library/src/public-api.ts @@ -6,3 +6,4 @@ export * from './icons/icon-type'; export * from './icons/icon-registry.service'; export * from './icons/icon-library.module'; export * from './icons/testing/icon-library-testing.module'; +export * from './images/image-type'; diff --git a/projects/components/src/load-async/load-async.directive.test.ts b/projects/components/src/load-async/load-async.directive.test.ts index 901d5a2fc..1912327b1 100644 --- a/projects/components/src/load-async/load-async.directive.test.ts +++ b/projects/components/src/load-async/load-async.directive.test.ts @@ -1,7 +1,7 @@ import { CommonModule } from '@angular/common'; import { HttpClientTestingModule } from '@angular/common/http/testing'; import { fakeAsync, tick } from '@angular/core/testing'; -import { IconLibraryTestingModule, IconType } from '@hypertrace/assets-library'; +import { IconLibraryTestingModule, IconType, ImagesAssetPath, LoaderType } from '@hypertrace/assets-library'; import { CustomError, NavigationService } from '@hypertrace/common'; import { createDirectiveFactory, mockProvider, SpectatorDirective } from '@ngneat/spectator/jest'; import { EMPTY, Observable, of, throwError } from 'rxjs'; @@ -25,7 +25,7 @@ describe('Load Async directive', () => { beforeEach(() => { spectator = createDirective(` -
+
{{ data }}
`); @@ -35,7 +35,12 @@ describe('Load Async directive', () => { spectator.setHostInput({ data$: of('content').pipe(delay(0)) }); + expect(spectator.query(LoaderComponent)).toExist(); + expect(spectator.query('.ht-loader img')).toExist(); + expect(spectator.query('.ht-loader img')).toHaveClass(LoaderType.Page); + expect(spectator.query('.ht-loader img')).toHaveAttribute('src', ImagesAssetPath.LoaderPage); + expect(spectator.query('.test-data')).not.toExist(); tick(); spectator.detectChanges(); diff --git a/projects/components/src/load-async/load-async.directive.ts b/projects/components/src/load-async/load-async.directive.ts index 3feff95d0..decc58602 100644 --- a/projects/components/src/load-async/load-async.directive.ts +++ b/projects/components/src/load-async/load-async.directive.ts @@ -9,6 +9,7 @@ import { TemplateRef, ViewContainerRef } from '@angular/core'; +import { LoaderType } from '@hypertrace/assets-library'; import { Observable, ReplaySubject } from 'rxjs'; import { LoadAsyncContext, LoadAsyncService } from './load-async.service'; import { @@ -23,8 +24,11 @@ import { export class LoadAsyncDirective implements OnChanges, OnDestroy { @Input('htLoadAsync') public data$?: Observable; + @Input('htLoadAsyncLoaderType') + public loaderType?: LoaderType; + private readonly wrapperParamsSubject: ReplaySubject = new ReplaySubject(1); - private readonly wrapperInjector: Injector; + private readonly wrapperInjector!: Injector; private wrapperView?: ComponentRef; public constructor( @@ -49,6 +53,7 @@ export class LoadAsyncDirective implements OnChanges, OnDestroy { this.wrapperView = this.wrapperView || this.buildWrapperView(); this.wrapperParamsSubject.next({ state$: this.loadAsyncService.mapObservableState(this.data$), + loaderType: this.loaderType, content: this.templateRef }); } else { diff --git a/projects/components/src/load-async/loader/loader.component.scss b/projects/components/src/load-async/loader/loader.component.scss index c09c207c2..f9c88ac07 100644 --- a/projects/components/src/load-async/loader/loader.component.scss +++ b/projects/components/src/load-async/loader/loader.component.scss @@ -7,4 +7,19 @@ flex-direction: column; justify-content: center; align-items: center; + + .page { + height: 50px; + width: 50px; + } + + .spinner { + height: 20px; + width: 20px; + } + + .expandable-row { + height: 20px; + width: auto; + } } diff --git a/projects/components/src/load-async/loader/loader.component.test.ts b/projects/components/src/load-async/loader/loader.component.test.ts new file mode 100644 index 000000000..5e7594b92 --- /dev/null +++ b/projects/components/src/load-async/loader/loader.component.test.ts @@ -0,0 +1,49 @@ +import { CommonModule } from '@angular/common'; +import { ImagesAssetPath, LoaderType } from '@hypertrace/assets-library'; +import { createHostFactory, SpectatorHost } from '@ngneat/spectator/jest'; +import { LoaderComponent } from './loader.component'; + +describe('Loader component', () => { + let spectator: SpectatorHost; + + const createHost = createHostFactory({ + component: LoaderComponent, + imports: [CommonModule] + }); + + test('Loader component when loader type is page', () => { + spectator = createHost(``); + + expect(spectator.query('.ht-loader')).toExist(); + expect(spectator.query('.ht-loader img')).toExist(); + expect(spectator.query('.ht-loader img')).toHaveClass(LoaderType.Page); + expect(spectator.query('.ht-loader img')).toHaveAttribute('src', ImagesAssetPath.LoaderPage); + }); + + test('Loader component when loader type is not passed', () => { + spectator = createHost(``); + + expect(spectator.query('.ht-loader')).toExist(); + expect(spectator.query('.ht-loader img')).toExist(); + expect(spectator.query('.ht-loader img')).toHaveClass(LoaderType.Spinner); + expect(spectator.query('.ht-loader img')).toHaveAttribute('src', ImagesAssetPath.LoaderSpinner); + }); + + test('Loader component when loader type is spinner', () => { + spectator = createHost(``); + + expect(spectator.query('.ht-loader')).toExist(); + expect(spectator.query('.ht-loader img')).toExist(); + expect(spectator.query('.ht-loader img')).toHaveClass(LoaderType.Spinner); + expect(spectator.query('.ht-loader img')).toHaveAttribute('src', ImagesAssetPath.LoaderSpinner); + }); + + test('Loader component loader type is expandable row', () => { + spectator = createHost(``); + + expect(spectator.query('.ht-loader')).toExist(); + expect(spectator.query('.ht-loader img')).toExist(); + expect(spectator.query('.ht-loader img')).toHaveClass(LoaderType.ExpandableRow); + expect(spectator.query('.ht-loader img')).toHaveAttribute('src', ImagesAssetPath.LoaderExpandableRow); + }); +}); diff --git a/projects/components/src/load-async/loader/loader.component.ts b/projects/components/src/load-async/loader/loader.component.ts index 6834f7f3d..1c3ad5195 100644 --- a/projects/components/src/load-async/loader/loader.component.ts +++ b/projects/components/src/load-async/loader/loader.component.ts @@ -1,15 +1,37 @@ -import { ChangeDetectionStrategy, Component } from '@angular/core'; -import { IconType } from '@hypertrace/assets-library'; -import { IconSize } from '../../icon/icon-size'; +import { ChangeDetectionStrategy, Component, Input, OnChanges } from '@angular/core'; +import { ImagesAssetPath, LoaderType } from '@hypertrace/assets-library'; +import { assertUnreachable } from '@hypertrace/common'; @Component({ selector: 'ht-loader', styleUrls: ['./loader.component.scss'], template: `
- +
`, changeDetection: ChangeDetectionStrategy.OnPush }) -export class LoaderComponent {} +export class LoaderComponent implements OnChanges { + @Input() + public type: LoaderType = LoaderType.Spinner; + + public imagePath: ImagesAssetPath = ImagesAssetPath.LoaderSpinner; + + public ngOnChanges(): void { + this.imagePath = this.getImagePathFromType(); + } + + private getImagePathFromType(): ImagesAssetPath { + switch (this.type) { + case LoaderType.ExpandableRow: + return ImagesAssetPath.LoaderExpandableRow; + case LoaderType.Page: + return ImagesAssetPath.LoaderPage; + case LoaderType.Spinner: + return ImagesAssetPath.LoaderSpinner; + default: + return assertUnreachable(this.type); + } + } +} diff --git a/projects/components/src/load-async/wrapper/load-async-wrapper.component.ts b/projects/components/src/load-async/wrapper/load-async-wrapper.component.ts index 8fe07ed85..2cf5a88f7 100644 --- a/projects/components/src/load-async/wrapper/load-async-wrapper.component.ts +++ b/projects/components/src/load-async/wrapper/load-async-wrapper.component.ts @@ -1,6 +1,6 @@ import { ChangeDetectionStrategy, Component, Inject, InjectionToken, TemplateRef } from '@angular/core'; -import { IconType } from '@hypertrace/assets-library'; -import { Observable } from 'rxjs'; +import { IconType, LoaderType } from '@hypertrace/assets-library'; +import { BehaviorSubject, Observable } from 'rxjs'; import { switchMap, tap } from 'rxjs/operators'; import { LoadAsyncStateType } from '../load-async-state.type'; import { AsyncState, ErrorAsyncState, LoadAsyncContext } from '../load-async.service'; @@ -8,13 +8,14 @@ import { AsyncState, ErrorAsyncState, LoadAsyncContext } from '../load-async.ser export const ASYNC_WRAPPER_PARAMETERS$ = new InjectionToken>( 'ASYNC_WRAPPER_PARAMETERS$' ); - @Component({ selector: 'ht-load-async-wrapper', template: `
- + + + @@ -39,9 +40,15 @@ export class LoadAsyncWrapperComponent { public content?: TemplateRef; + private readonly loaderTypeSubject: BehaviorSubject = new BehaviorSubject< + LoaderType | undefined + >(undefined); + public readonly loaderType$: Observable = this.loaderTypeSubject.asObservable(); + public constructor(@Inject(ASYNC_WRAPPER_PARAMETERS$) parameters$: Observable) { this.state$ = parameters$.pipe( tap(params => (this.content = params.content)), + tap(params => this.loaderTypeSubject.next(params.loaderType)), switchMap(parameter => parameter.state$), tap(state => this.updateMessage(state.type, (state as Partial).description)) ); @@ -65,5 +72,6 @@ export class LoadAsyncWrapperComponent { export interface LoadAsyncWrapperParameters { state$: Observable; + loaderType?: LoaderType; content: TemplateRef; } diff --git a/projects/components/src/not-found/not-found.component.ts b/projects/components/src/not-found/not-found.component.ts index b5963a383..79f59add9 100644 --- a/projects/components/src/not-found/not-found.component.ts +++ b/projects/components/src/not-found/not-found.component.ts @@ -1,7 +1,7 @@ import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { ImagesAssetPath } from '@hypertrace/assets-library'; import { NavigationService } from '@hypertrace/common'; import { ButtonRole, ButtonStyle } from '../button/button'; - @Component({ selector: 'ht-not-found', changeDetection: ChangeDetectionStrategy.OnPush, @@ -9,7 +9,7 @@ import { ButtonRole, ButtonStyle } from '../button/button'; template: `
- not found page + not found page
Page not found