Skip to content

feat: custom messages for load async #1217

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Oct 29, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions projects/assets-library/src/images/image-type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const enum ImagesAssetPath {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How is this different than icon? I think I missed the context of this change

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one, I was not very keen on. But Michael had the new loaders as gifs. So, we would have to use image. ht-icon won't work as per Alok. I think he tried.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you remember why it wouldn't work? I think icon already handles svgs and font ligatures, it just may need some changes to add gif support.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't remember. @itssharmasandeep Can we try using ht-icons for this?

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'
}
1 change: 1 addition & 0 deletions projects/assets-library/src/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
9 changes: 7 additions & 2 deletions projects/components/src/load-async/load-async.directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
ViewContainerRef
} from '@angular/core';
import { Observable, ReplaySubject } from 'rxjs';
import { LoadAsyncContext, LoadAsyncService } from './load-async.service';
import { LoadAsyncConfig, LoadAsyncContext, LoadAsyncService } from './load-async.service';
import {
ASYNC_WRAPPER_PARAMETERS$,
LoadAsyncWrapperComponent,
Expand All @@ -23,6 +23,10 @@ import {
export class LoadAsyncDirective implements OnChanges, OnDestroy {
@Input('htLoadAsync')
public data$?: Observable<unknown>;

// tslint:disable-next-line:no-input-rename
@Input('htLoadAsyncConfig')
public config?: LoadAsyncConfig;
private readonly wrapperParamsSubject: ReplaySubject<LoadAsyncWrapperParameters> = new ReplaySubject(1);
private readonly wrapperInjector: Injector;
private wrapperView?: ComponentRef<LoadAsyncWrapperComponent>;
Expand All @@ -49,7 +53,8 @@ export class LoadAsyncDirective implements OnChanges, OnDestroy {
this.wrapperView = this.wrapperView || this.buildWrapperView();
this.wrapperParamsSubject.next({
state$: this.loadAsyncService.mapObservableState(this.data$),
content: this.templateRef
content: this.templateRef,
config: this.config
});
} else {
// If observable is cleared, clear the DOM
Expand Down
28 changes: 25 additions & 3 deletions projects/components/src/load-async/load-async.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Injectable } from '@angular/core';
import { IconType } from '@hypertrace/assets-library';
import { CustomError } from '@hypertrace/common';
import { Observable, of } from 'rxjs';
import { catchError, defaultIfEmpty, map, startWith } from 'rxjs/operators';
Expand Down Expand Up @@ -50,18 +51,39 @@ export interface LoadAsyncContext {
$implicit: unknown;
}

export type AsyncState = ErrorAsyncState | SuccessAsyncState | LoadingAsyncState;
export interface LoadAsyncConfig {
load?: LoadingStateConfig;
noData?: NoDataOrErrorStateConfig;
error?: NoDataOrErrorStateConfig;
}

export type AsyncState = LoadingAsyncState | SuccessAsyncState | NoDataOrErrorAsyncState;

export const enum LoaderType {
Spinner = 'spinner',
ExpandableRow = 'expandable-row',
Page = 'page'
}

interface LoadingAsyncState {
type: LoadAsyncStateType.Loading;
}

interface SuccessAsyncState {
type: LoadAsyncStateType.Success;
context: LoadAsyncContext;
}

export interface ErrorAsyncState {
interface NoDataOrErrorAsyncState {
type: LoadAsyncStateType.GenericError | LoadAsyncStateType.NoData;
description?: string;
}

interface LoadingStateConfig {
loaderType?: LoaderType;
}

interface NoDataOrErrorStateConfig {
icon?: IconType;
title?: string;
description?: string;
}
15 changes: 15 additions & 0 deletions projects/components/src/load-async/loader/loader.component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { CommonModule } from '@angular/common';
import { ImagesAssetPath } from '@hypertrace/assets-library';
import { createHostFactory, SpectatorHost } from '@ngneat/spectator/jest';
import { LoaderType } from '../load-async.service';
import { LoaderComponent } from './loader.component';

describe('Loader component', () => {
let spectator: SpectatorHost<LoaderComponent>;

const createHost = createHostFactory({
component: LoaderComponent,
imports: [CommonModule]
});

test('Loader component when loader type is page', () => {
spectator = createHost(`<ht-loader [loaderType]="'${LoaderType.Page}'"></ht-loader>`);

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(`<ht-loader></ht-loader>`);

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(`<ht-loader [loaderType]="'${LoaderType.Spinner}'"></ht-loader>`);

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(`<ht-loader [loaderType]="'${LoaderType.ExpandableRow}'"></ht-loader>`);

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);
});
});
35 changes: 28 additions & 7 deletions projects/components/src/load-async/loader/loader.component.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,36 @@
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 } from '@hypertrace/assets-library';
import { LoaderType } from '../load-async.service';

@Component({
selector: 'ht-loader',
styleUrls: ['./loader.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="ht-loader">
<ht-icon icon="${IconType.Loading}" size="${IconSize.Large}"></ht-icon>
<img [ngClass]="[this.currentLoaderType]" [src]="this.getImagePathFromType(this.currentLoaderType)" />
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush
`
})
export class LoaderComponent {}
export class LoaderComponent implements OnChanges {
@Input()
public loaderType?: LoaderType;

public currentLoaderType: LoaderType = LoaderType.Spinner;

public ngOnChanges(): void {
this.currentLoaderType = this.loaderType ?? LoaderType.Spinner;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could just set the image path here. no need to maintain currentLoaderType

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#1217 (comment)

We needed this, as if someone passes the undefined to loaderType then it will come to ngOnChanges, now with the previous approach, the image path will be set to ImageAssetPath.LoaderSpinner since we're returning as default from switch case, but the loaderType will be undefined only and it will throw an error for ngClass,
Because we're doing [ngClass]="[this.loaderType]"

So I needed to reassign this var.

}

public getImagePathFromType(loaderType: LoaderType): ImagesAssetPath {
switch (loaderType) {
case LoaderType.ExpandableRow:
return ImagesAssetPath.LoaderExpandableRow;
case LoaderType.Page:
return ImagesAssetPath.LoaderPage;
case LoaderType.Spinner:
default:
return ImagesAssetPath.LoaderSpinner;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { IconType } from '@hypertrace/assets-library';
import { Observable } from 'rxjs';
import { switchMap, tap } from 'rxjs/operators';
import { LoadAsyncStateType } from '../load-async-state.type';
import { AsyncState, ErrorAsyncState, LoadAsyncContext } from '../load-async.service';
import { AsyncState, LoadAsyncConfig, LoadAsyncContext, LoaderType } from '../load-async.service';

export const ASYNC_WRAPPER_PARAMETERS$ = new InjectionToken<Observable<LoadAsyncWrapperParameters>>(
'ASYNC_WRAPPER_PARAMETERS$'
Expand All @@ -14,7 +14,7 @@ export const ASYNC_WRAPPER_PARAMETERS$ = new InjectionToken<Observable<LoadAsync
template: `
<div *ngIf="this.state$ | async as state" class="fill-container" [ngSwitch]="state.type">
<ng-container *ngSwitchCase="'${LoadAsyncStateType.Loading}'">
<ht-loader></ht-loader>
<ht-loader [loaderType]="this.loaderType"></ht-loader>
</ng-container>
<ng-container *ngSwitchCase="'${LoadAsyncStateType.Success}'">
<ng-container *ngTemplateOutlet="this.content; context: state.context"></ng-container>
Expand All @@ -34,36 +34,49 @@ export class LoadAsyncWrapperComponent {
public readonly state$: Observable<AsyncState>;

public icon?: IconType;
public loaderType?: LoaderType;
public title?: string;
public description: string = '';

public content?: TemplateRef<LoadAsyncContext>;
public config?: LoadAsyncConfig;

public constructor(@Inject(ASYNC_WRAPPER_PARAMETERS$) parameters$: Observable<LoadAsyncWrapperParameters>) {
this.state$ = parameters$.pipe(
tap(params => (this.content = params.content)),
tap(params => {
this.content = params.content;
this.config = params.config;
}),
switchMap(parameter => parameter.state$),
tap(state => this.updateMessage(state.type, (state as Partial<ErrorAsyncState>).description))
tap(state => this.updateMessage(state))
);
}

private updateMessage(stateType: LoadAsyncStateType, description: string = ''): void {
this.description = description;

switch (stateType) {
private updateMessage(state: AsyncState): void {
switch (state.type) {
case LoadAsyncStateType.Loading:
this.loaderType = this.config?.load?.loaderType;
break;
case LoadAsyncStateType.NoData:
this.icon = IconType.NoData;
this.title = 'No Data';
this.icon = this.config?.noData?.icon ?? IconType.NoData;
this.title = this.config?.noData?.title ?? 'No Data';
this.description = this.config?.noData?.description ?? '';
break;
case LoadAsyncStateType.GenericError:
this.icon = this.config?.error?.icon ?? IconType.Error;
this.title = this.config?.error?.title ?? 'Error';
this.description = state.description ?? this.config?.error?.description ?? '';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went back and forth which order these two should come in. I think I'm leaning towards a configured error description should take precedence over one from an error itself. We can leave it as is for now, may change later.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I was also confused about that!!

break;
default:
this.icon = IconType.Error;
this.title = 'Error';
this.icon = undefined;
this.title = '';
this.description = '';
}
}
}

export interface LoadAsyncWrapperParameters {
state$: Observable<AsyncState>;
content: TemplateRef<LoadAsyncContext>;
config?: LoadAsyncConfig;
}
3 changes: 2 additions & 1 deletion projects/components/src/not-found/not-found.component.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { ImagesAssetPath } from '@hypertrace/assets-library';
import { NavigationService } from '@hypertrace/common';
import { ButtonRole, ButtonStyle } from '../button/button';

Expand All @@ -9,7 +10,7 @@ import { ButtonRole, ButtonStyle } from '../button/button';
template: `
<div class="not-found-container fill-container">
<div class="not-found-content">
<img class="not-found-image" src="assets/images/error-page.svg" loading="lazy" alt="not found page" />
<img class="not-found-image" src="${ImagesAssetPath.ErrorPage}" loading="lazy" alt="not found page" />
<div class="not-found-message-wrapper">
<div class="not-found-text-wrapper">
<div class="not-found-message">Page not found</div>
Expand Down
6 changes: 4 additions & 2 deletions projects/components/src/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,8 +171,10 @@ export * from './list-view/list-view.component';
export * from './list-view/list-view.module';

// Load Async
export { LoadAsyncDirective } from './load-async/load-async.directive';
export { LoadAsyncModule } from './load-async/load-async.module';
export * from './load-async/load-async.directive';
export * from './load-async/load-async.module';
export * from './load-async/load-async.service';
export * from './load-async/load-async-state.type';

// Message Display
export { MessageDisplayComponent } from './message-display/message-display.component';
Expand Down
9 changes: 8 additions & 1 deletion projects/components/src/table/table.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { without } from 'lodash-es';
import { BehaviorSubject, combineLatest, merge, Observable, Subject } from 'rxjs';
import { filter, map } from 'rxjs/operators';
import { FilterAttribute } from '../filtering/filter/filter-attribute';
import { LoadAsyncConfig } from '../load-async/load-async.service';
import { PageEvent } from '../paginator/page.event';
import { PaginatorComponent } from '../paginator/paginator.component';
import { CoreTableCellRendererType } from './cells/types/core-table-cell-renderer-type';
Expand Down Expand Up @@ -162,7 +163,10 @@ import { TableColumnConfigExtended, TableService } from './table.service';
<!-- State Watcher -->
<ng-container *ngIf="this.dataSource?.loadingStateChange$ | async as loadingState">
<div class="state-watcher" *ngIf="!loadingState.hide">
<ng-container class="state-watcher" *htLoadAsync="loadingState.loading$"></ng-container>
<ng-container
class="state-watcher"
*htLoadAsync="loadingState.loading$; config: this.loadingConfig"
></ng-container>
</div>
</ng-container>

Expand Down Expand Up @@ -272,6 +276,9 @@ export class TableComponent
@Input()
public pageSize?: number = 50;

@Input()
public loadingConfig?: LoadAsyncConfig;

@Output()
public readonly rowClicked: EventEmitter<StatefulTableRow> = new EventEmitter<StatefulTableRow>();

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { TableDataSource, TableMode, TableRow, TableSelectionMode, TableStyle } from '@hypertrace/components';
import {
LoadAsyncConfig,
TableDataSource,
TableMode,
TableRow,
TableSelectionMode,
TableStyle
} from '@hypertrace/components';
import {
ArrayPropertyTypeInstance,
BaseModel,
Expand All @@ -13,6 +20,7 @@ import {
ModelModelPropertyTypeInstance,
ModelProperty,
ModelPropertyType,
PLAIN_OBJECT_PROPERTY,
STRING_PROPERTY
} from '@hypertrace/hyperdash';
import { ModelInject, MODEL_API } from '@hypertrace/hyperdash-angular';
Expand Down Expand Up @@ -118,6 +126,13 @@ export abstract class TableWidgetBaseModel extends BaseModel {
})
public resizable: boolean = true;

@ModelProperty({
key: 'loadingConfig',
required: false,
type: PLAIN_OBJECT_PROPERTY.type
})
public loadingConfig?: LoadAsyncConfig;

@ModelInject(MODEL_API)
protected readonly api!: ModelApi;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ import { TableWidgetModel } from './table-widget.model';
[selectionMode]="this.model.getSelectionMode()"
[display]="this.model.style"
[data]="this.data$ | async"
[loadingConfig]="this.model.loadingConfig"
[filters]="this.combinedFilters$ | async"
[queryProperties]="this.queryProperties$ | async"
[pageable]="this.api.model.isPageable()"
Expand Down