Browse Source

Merge pull request #6798 from abpframework/feat/6733

add wait services
pull/6805/head
Levent Arman Özak 6 years ago
committed by GitHub
parent
commit
6066f0d61c
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 10
      npm/ng-packs/packages/core/src/lib/interceptors/api.interceptor.ts
  2. 44
      npm/ng-packs/packages/core/src/lib/services/http-wait.service.ts
  3. 3
      npm/ng-packs/packages/core/src/lib/services/index.ts
  4. 10
      npm/ng-packs/packages/core/src/lib/services/lazy-load.service.ts
  5. 41
      npm/ng-packs/packages/core/src/lib/services/resource-wait.service.ts
  6. 53
      npm/ng-packs/packages/core/src/lib/services/router-wait.service.ts
  7. 19
      npm/ng-packs/packages/core/src/lib/tests/api.interceptor.spec.ts
  8. 7
      npm/ng-packs/packages/core/src/lib/tests/lazy-load.service.spec.ts
  9. 60
      npm/ng-packs/packages/theme-shared/src/lib/components/loader-bar/loader-bar.component.ts
  10. 47
      npm/ng-packs/packages/theme-shared/src/lib/tests/loader-bar.component.spec.ts

10
npm/ng-packs/packages/core/src/lib/interceptors/api.interceptor.ts

@ -1,10 +1,9 @@
import { HttpHandler, HttpHeaders, HttpInterceptor, HttpRequest } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Store } from '@ngxs/store';
import { OAuthService } from 'angular-oauth2-oidc';
import { finalize } from 'rxjs/operators';
import { StartLoader, StopLoader } from '../actions/loader.actions';
import { SessionStateService } from '../services/session-state.service';
import { HttpWaitService } from '../services/http-wait.service';
@Injectable({
providedIn: 'root',
@ -12,20 +11,19 @@ import { SessionStateService } from '../services/session-state.service';
export class ApiInterceptor implements HttpInterceptor {
constructor(
private oAuthService: OAuthService,
private store: Store,
private sessionState: SessionStateService,
private httpWaitService: HttpWaitService,
) {}
intercept(request: HttpRequest<any>, next: HttpHandler) {
this.store.dispatch(new StartLoader(request));
this.httpWaitService.addRequest(request);
return next
.handle(
request.clone({
setHeaders: this.getAdditionalHeaders(request.headers),
}),
)
.pipe(finalize(() => this.store.dispatch(new StopLoader(request))));
.pipe(finalize(() => this.httpWaitService.deleteRequest(request)));
}
getAdditionalHeaders(existingHeaders?: HttpHeaders) {

44
npm/ng-packs/packages/core/src/lib/services/http-wait.service.ts

@ -0,0 +1,44 @@
import { Injectable } from '@angular/core';
import { HttpRequest } from '@angular/common/http';
import { InternalStore } from '../utils/internal-store-utils';
export interface HttpWaitState {
requests: Set<HttpRequest<any>>;
}
@Injectable({
providedIn: 'root',
})
export class HttpWaitService {
protected store = new InternalStore<HttpWaitState>({ requests: new Set() });
getLoading() {
return !!this.store.state.requests.size;
}
getLoading$() {
return this.store.sliceState(({ requests }) => !!requests.size);
}
updateLoading$() {
return this.store.sliceUpdate(({ requests }) => !!requests.size);
}
clearLoading() {
this.store.patch({ requests: new Set() });
}
addRequest(request: HttpRequest<any>) {
const requests = this.store.state.requests;
requests.add(request);
this.store.patch({ requests });
}
deleteRequest(request: HttpRequest<any>) {
const requests = this.store.state.requests;
requests.delete(request);
this.store.patch({ requests });
}
// TODO: Add filter function
}

3
npm/ng-packs/packages/core/src/lib/services/index.ts

@ -4,6 +4,7 @@ export * from './config-state.service';
export * from './content-projection.service';
export * from './dom-insertion.service';
export * from './environment.service';
export * from './http-wait.service';
export * from './lazy-load.service';
export * from './list.service';
export * from './localization.service';
@ -12,7 +13,9 @@ export * from './permission.service';
export * from './profile-state.service';
export * from './profile.service';
export * from './replaceable-components.service';
export * from './resource-wait.service';
export * from './rest.service';
export * from './router-wait.service';
export * from './routes.service';
export * from './session-state.service';
export * from './subscription.service';

10
npm/ng-packs/packages/core/src/lib/services/lazy-load.service.ts

@ -2,6 +2,7 @@ import { Injectable } from '@angular/core';
import { concat, Observable, of, throwError } from 'rxjs';
import { delay, retryWhen, shareReplay, take, tap } from 'rxjs/operators';
import { LoadingStrategy } from '../strategies';
import { ResourceWaitService } from './resource-wait.service';
@Injectable({
providedIn: 'root',
@ -9,9 +10,11 @@ import { LoadingStrategy } from '../strategies';
export class LazyLoadService {
readonly loaded = new Map<string, HTMLScriptElement | HTMLLinkElement>();
constructor(private resourceWaitService: ResourceWaitService) {}
load(strategy: LoadingStrategy, retryTimes?: number, retryDelay?: number): Observable<Event> {
if (this.loaded.has(strategy.path)) return of(new CustomEvent('load'));
this.resourceWaitService.addResource(strategy.path);
return strategy.createStream().pipe(
retryWhen(error$ =>
concat(
@ -19,7 +22,10 @@ export class LazyLoadService {
throwError(new CustomEvent('error')),
),
),
tap(() => this.loaded.set(strategy.path, strategy.element)),
tap(() => {
this.loaded.set(strategy.path, strategy.element);
this.resourceWaitService.deleteResource(strategy.path);
}),
delay(100),
shareReplay({ bufferSize: 1, refCount: true }),
);

41
npm/ng-packs/packages/core/src/lib/services/resource-wait.service.ts

@ -0,0 +1,41 @@
import { Injectable } from '@angular/core';
import { InternalStore } from '../utils/internal-store-utils';
export interface ResourceWaitState {
resources: Set<string>;
}
@Injectable({
providedIn: 'root',
})
export class ResourceWaitService {
private store = new InternalStore<ResourceWaitState>({ resources: new Set() });
getLoading() {
return !!this.store.state.resources.size;
}
getLoading$() {
return this.store.sliceState(({ resources }) => !!resources.size);
}
updateLoading$() {
return this.store.sliceUpdate(({ resources }) => !!resources.size);
}
clearLoading() {
this.store.patch({ resources: new Set() });
}
addResource(resource: string) {
const resources = this.store.state.resources;
resources.add(resource);
this.store.patch({ resources });
}
deleteResource(resource: string) {
const resources = this.store.state.resources;
resources.delete(resource);
this.store.patch({ resources });
}
}

53
npm/ng-packs/packages/core/src/lib/services/router-wait.service.ts

@ -0,0 +1,53 @@
import { Injectable } from '@angular/core';
import {
NavigationCancel,
NavigationEnd,
NavigationError,
NavigationStart,
Router,
} from '@angular/router';
import { filter } from 'rxjs/operators';
import { InternalStore } from '../utils/internal-store-utils';
export interface RouterWaitState {
loading: boolean;
}
@Injectable({
providedIn: 'root',
})
export class RouterWaitService {
private store = new InternalStore<RouterWaitState>({ loading: false });
constructor(private router: Router) {
this.router.events
.pipe(
filter(
event =>
event instanceof NavigationStart ||
event instanceof NavigationEnd ||
event instanceof NavigationError ||
event instanceof NavigationCancel,
),
)
.subscribe(event => {
if (event instanceof NavigationStart) this.setLoading(true);
else this.setLoading(false);
});
}
getLoading() {
return this.store.state.loading;
}
getLoading$() {
return this.store.sliceState(({ loading }) => loading);
}
updateLoading$() {
return this.store.sliceUpdate(({ loading }) => loading);
}
setLoading(loading: boolean) {
this.store.patch({ loading });
}
}

19
npm/ng-packs/packages/core/src/lib/tests/api.interceptor.spec.ts

@ -1,31 +1,29 @@
import { HttpRequest } from '@angular/common/http';
import { SpyObject } from '@ngneat/spectator';
import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest';
import { Store } from '@ngxs/store';
import { OAuthService } from 'angular-oauth2-oidc';
import { Subject, timer } from 'rxjs';
import { StartLoader, StopLoader } from '../actions';
import { ApiInterceptor } from '../interceptors';
import { SessionStateService } from '../services';
import { HttpWaitService, SessionStateService } from '../services';
describe('ApiInterceptor', () => {
let spectator: SpectatorService<ApiInterceptor>;
let interceptor: ApiInterceptor;
let store: SpyObject<Store>;
let oauthService: SpyObject<OAuthService>;
let sessionState: SpyObject<SessionStateService>;
let httpWaitService: SpyObject<HttpWaitService>;
const createService = createServiceFactory({
service: ApiInterceptor,
mocks: [OAuthService, Store, SessionStateService],
mocks: [OAuthService, SessionStateService],
});
beforeEach(() => {
spectator = createService();
interceptor = spectator.service;
store = spectator.inject(Store);
sessionState = spectator.inject(SessionStateService);
oauthService = spectator.inject(OAuthService);
httpWaitService = spectator.inject(HttpWaitService);
});
it('should add headers to http request', done => {
@ -52,8 +50,9 @@ describe('ApiInterceptor', () => {
handleRes$.complete();
});
it('should dispatch the loader', done => {
const spy = jest.spyOn(store, 'dispatch');
it('should call http wait services add request and delete request', done => {
const spyAddRequest = jest.spyOn(httpWaitService, 'addRequest');
const spyDeleteRequest = jest.spyOn(httpWaitService, 'deleteRequest');
const request = new HttpRequest('GET', 'https://abp.io');
const handleRes$ = new Subject();
@ -70,8 +69,8 @@ describe('ApiInterceptor', () => {
handleRes$.complete();
timer(0).subscribe(() => {
expect(spy.mock.calls[0][0] instanceof StartLoader).toBeTruthy();
expect(spy.mock.calls[1][0] instanceof StopLoader).toBeTruthy();
expect(spyAddRequest).toHaveBeenCalled();
expect(spyDeleteRequest).toHaveBeenCalled();
done();
});
});

7
npm/ng-packs/packages/core/src/lib/tests/lazy-load.service.spec.ts

@ -2,10 +2,12 @@ import { of, throwError } from 'rxjs';
import { switchMap } from 'rxjs/operators';
import { LazyLoadService } from '../services/lazy-load.service';
import { ScriptLoadingStrategy } from '../strategies';
import { ResourceWaitService } from '../services';
describe('LazyLoadService', () => {
describe('#load', () => {
const service = new LazyLoadService();
const resourceWaitService = new ResourceWaitService();
const service = new LazyLoadService(resourceWaitService);
const strategy = new ScriptLoadingStrategy('http://example.com/');
afterEach(() => {
@ -58,7 +60,8 @@ describe('LazyLoadService', () => {
});
describe('#remove', () => {
const service = new LazyLoadService();
const resourceWaitService = new ResourceWaitService();
const service = new LazyLoadService(resourceWaitService);
it('should remove an already lazy loaded element and return true', () => {
const script = document.createElement('script');

60
npm/ng-packs/packages/theme-shared/src/lib/components/loader-bar/loader-bar.component.ts

@ -1,9 +1,7 @@
import { StartLoader, StopLoader, SubscriptionService } from '@abp/ng.core';
import { HttpWaitService, RouterWaitService, SubscriptionService } from '@abp/ng.core';
import { ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core';
import { NavigationEnd, NavigationError, NavigationStart, Router } from '@angular/router';
import { Actions, ofActionSuccessful } from '@ngxs/store';
import { Subscription, timer } from 'rxjs';
import { filter } from 'rxjs/operators';
import { Router } from '@angular/router';
import { combineLatest, Subscription, timer } from 'rxjs';
@Component({
selector: 'abp-loader-bar',
@ -43,18 +41,14 @@ export class LoaderBarComponent implements OnDestroy, OnInit {
progressLevel = 0;
interval: Subscription;
interval = new Subscription();
timer: Subscription;
timer = new Subscription();
intervalPeriod = 350;
stopDelay = 800;
@Input()
filter = (action: StartLoader | StopLoader) =>
action.payload.url.indexOf('openid-configuration') < 0;
private readonly clearProgress = () => {
this.progressLevel = 0;
this.cdRef.detectChanges();
@ -78,63 +72,47 @@ export class LoaderBarComponent implements OnDestroy, OnInit {
}
constructor(
private actions: Actions,
private router: Router,
private cdRef: ChangeDetectorRef,
private subscription: SubscriptionService,
private httpWaitService: HttpWaitService,
private routerWaiterService: RouterWaitService,
) {}
private subscribeToLoadActions() {
this.subscription.addOne(
this.actions.pipe(ofActionSuccessful(StartLoader, StopLoader), filter(this.filter)),
action => {
if (action instanceof StartLoader) this.startLoading();
else this.stopLoading();
},
);
ngOnInit() {
this.subscribeLoading();
}
private subscribeToRouterEvents() {
subscribeLoading() {
this.subscription.addOne(
this.router.events.pipe(
filter(
event =>
event instanceof NavigationStart ||
event instanceof NavigationEnd ||
event instanceof NavigationError,
),
),
event => {
if (event instanceof NavigationStart) this.startLoading();
combineLatest([this.httpWaitService.getLoading$(), this.routerWaiterService.getLoading$()]),
([httpLoading, routerLoading]) => {
if (httpLoading || routerLoading) this.startLoading();
else this.stopLoading();
},
);
}
ngOnInit() {
this.subscribeToLoadActions();
this.subscribeToRouterEvents();
}
ngOnDestroy() {
if (this.interval) this.interval.unsubscribe();
this.interval.unsubscribe();
}
startLoading() {
if (this.isLoading || (this.interval && !this.interval.closed)) return;
if (this.isLoading || !this.interval.closed) return;
this.isLoading = true;
this.progressLevel = 0;
this.interval = timer(0, this.intervalPeriod).subscribe(this.reportProgress);
this.timer.unsubscribe();
}
stopLoading() {
if (this.interval) this.interval.unsubscribe();
this.interval.unsubscribe();
this.progressLevel = 100;
this.isLoading = false;
if (this.timer && !this.timer.closed) return;
if (!this.timer.closed) return;
this.timer = timer(this.stopDelay).subscribe(this.clearProgress);
}

47
npm/ng-packs/packages/theme-shared/src/lib/tests/loader-bar.component.spec.ts

@ -1,47 +1,37 @@
import {
Router,
RouteReuseStrategy,
NavigationStart,
NavigationEnd,
NavigationError,
} from '@angular/router';
import { createHostFactory, SpectatorHost, SpyObject } from '@ngneat/spectator/jest';
import { Actions, NgxsModule, Store } from '@ngxs/store';
import { Subject, Subscription, Observable, Subscriber, timer } from 'rxjs';
import { NavigationEnd, NavigationError, NavigationStart, Router } from '@angular/router';
import { createComponentFactory, Spectator, SpyObject } from '@ngneat/spectator/jest';
import { Subject, timer } from 'rxjs';
import { LoaderBarComponent } from '../components/loader-bar/loader-bar.component';
import { StartLoader, StopLoader, SubscriptionService } from '@abp/ng.core';
import { HttpWaitService, SubscriptionService } from '@abp/ng.core';
import { HttpRequest } from '@angular/common/http';
describe('LoaderBarComponent', () => {
let spectator: SpectatorHost<LoaderBarComponent>;
let spectator: Spectator<LoaderBarComponent>;
let router: SpyObject<Router>;
const events$ = new Subject();
const createHost = createHostFactory({
const createComponent = createComponentFactory({
component: LoaderBarComponent,
mocks: [Router],
imports: [NgxsModule.forRoot()],
detectChanges: false,
providers: [SubscriptionService],
providers: [SubscriptionService, { provide: Router, useValue: { events: events$ } }],
});
beforeEach(() => {
spectator = createHost('<abp-loader-bar></abp-loader-bar>');
spectator = createComponent({});
spectator.component.intervalPeriod = 1;
spectator.component.stopDelay = 1;
router = spectator.inject(Router);
(router as any).events = events$;
});
it('should initial variable values are correct', () => {
spectator.component.interval = new Subscription();
expect(spectator.component.containerClass).toBe('abp-loader-bar');
expect(spectator.component.color).toBe('#77b6ff');
});
it('should increase the progressLevel', done => {
spectator.detectChanges();
spectator.inject(Store).dispatch(new StartLoader(new HttpRequest('GET', 'test')));
const httpWaitService = spectator.inject(HttpWaitService);
httpWaitService.addRequest(new HttpRequest('GET', 'test'));
spectator.detectChanges();
setTimeout(() => {
expect(spectator.component.progressLevel > 0).toBeTruthy();
@ -51,7 +41,8 @@ describe('LoaderBarComponent', () => {
test.skip('should be interval unsubscribed', done => {
spectator.detectChanges();
spectator.inject(Store).dispatch(new StartLoader(new HttpRequest('GET', 'test')));
const httpWaitService = spectator.inject(HttpWaitService);
httpWaitService.addRequest(new HttpRequest('GET', 'test'));
expect(spectator.component.interval.closed).toBe(false);
timer(400).subscribe(() => {
@ -62,11 +53,11 @@ describe('LoaderBarComponent', () => {
it('should start and stop the loading with navigation', done => {
spectator.detectChanges();
(router as any).events.next(new NavigationStart(1, 'test'));
events$.next(new NavigationStart(1, 'test'));
expect(spectator.component.interval.closed).toBe(false);
(router as any).events.next(new NavigationEnd(1, 'test', 'test'));
(router as any).events.next(new NavigationError(1, 'test', 'test'));
events$.next(new NavigationEnd(1, 'test', 'test'));
events$.next(new NavigationError(1, 'test', 'test'));
expect(spectator.component.progressLevel).toBe(100);
timer(2).subscribe(() => {
@ -77,10 +68,10 @@ describe('LoaderBarComponent', () => {
it('should stop the loading with navigation', done => {
spectator.detectChanges();
(router as any).events.next(new NavigationStart(1, 'test'));
events$.next(new NavigationStart(1, 'test'));
expect(spectator.component.interval.closed).toBe(false);
spectator.inject(Store).dispatch(new StopLoader(new HttpRequest('GET', 'test')));
events$.next(new NavigationEnd(1, 'testend', 'testend'));
expect(spectator.component.progressLevel).toBe(100);
timer(2).subscribe(() => {
@ -92,8 +83,8 @@ describe('LoaderBarComponent', () => {
describe('#startLoading', () => {
it('should return when isLoading is true', done => {
spectator.detectChanges();
(router as any).events.next(new NavigationStart(1, 'test'));
(router as any).events.next(new NavigationStart(1, 'test'));
events$.next(new NavigationStart(1, 'test'));
events$.next(new NavigationStart(1, 'test'));
done();
});
});

Loading…
Cancel
Save