add uber cache and some streams for test data
This commit is contained in:
@ -36,8 +36,9 @@
|
||||
"bootstrap-material-design": "4.1.1",
|
||||
"bootstrap-notify": "3.1.3",
|
||||
"chartist": "0.11.0",
|
||||
"classlist.js": "1.1.20150312",
|
||||
"classlist.js": "^1.1.20150312",
|
||||
"core-js": "2.4.1",
|
||||
"event-source-polyfill": "^1.0.5",
|
||||
"express": "4.16.3",
|
||||
"googleapis": "28.1.0",
|
||||
"hammerjs": "2.0.8",
|
||||
@ -47,10 +48,11 @@
|
||||
"popper.js": "1.14.3",
|
||||
"rxjs": "6.3.3",
|
||||
"rxjs-compat": "6.3.3",
|
||||
"web-animations-js": "2.3.1",
|
||||
"web-animations-js": "^2.3.1",
|
||||
"zone.js": "0.8.26"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "~0.6.3",
|
||||
"@angular/cli": "6.0.3",
|
||||
"@angular/compiler-cli": "7.0.2",
|
||||
"@angular/language-service": "7.0.2",
|
||||
@ -71,7 +73,6 @@
|
||||
"protractor": "5.3.1",
|
||||
"ts-node": "5.0.1",
|
||||
"tslint": "5.9.1",
|
||||
"typescript": "3.1.6",
|
||||
"@angular-devkit/build-angular": "~0.6.3"
|
||||
"typescript": "3.1.6"
|
||||
}
|
||||
}
|
||||
|
43
src/app/User.ts
Normal file
43
src/app/User.ts
Normal file
@ -0,0 +1,43 @@
|
||||
interface Claims {
|
||||
// https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
|
||||
sub?: string;
|
||||
name?: string;
|
||||
given_name?: string;
|
||||
family_name?: string;
|
||||
middle_name?: string;
|
||||
nickname?: string;
|
||||
preferred_username?: string;
|
||||
profile?: string; // url to their profile
|
||||
picture?: string; // url to their image ending in .png, .jpg, etc...
|
||||
website?: string;
|
||||
email: string;
|
||||
email_verified?: boolean;
|
||||
gender?: string; // usually 'male' or 'female'
|
||||
birthdate?: string; // 'yyyy-mm-dd'
|
||||
zoneinfo?: string; // e.g. 'Europe/Paris' or 'America/Los_Angeles'
|
||||
local?: string; // e.g. 'en-US' or 'fr-CA' some implementations us underscore
|
||||
phone_number?: string;
|
||||
phone_number_verified?: boolean;
|
||||
address?: Address;
|
||||
updated_at?: number; // seconds since the epoc when this object was last updated
|
||||
}
|
||||
|
||||
interface User {
|
||||
claims: Claims;
|
||||
customers: string[];
|
||||
}
|
||||
|
||||
interface Address {
|
||||
formatted: string;
|
||||
street_address: string;
|
||||
locality: string; // e.g. city
|
||||
region: string; // e.g. state, province, prefecture or region
|
||||
postal_code: string; // zip coed or postal code
|
||||
country: string;
|
||||
}
|
||||
|
||||
export {
|
||||
User,
|
||||
Claims,
|
||||
Address
|
||||
}
|
@ -2,6 +2,7 @@ import { Injectable } from '@angular/core';
|
||||
import { Observable, throwError } from 'rxjs';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
|
||||
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
|
||||
|
||||
import { AvailableAppModule } from './AppModulesTypes';
|
||||
|
||||
@ -9,9 +10,16 @@ import { AvailableAppModule } from './AppModulesTypes';
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AppModulesService {
|
||||
// default this value to the user's last time logging in
|
||||
private showTestDataSource = new BehaviorSubject<boolean>(true);
|
||||
public showTestData = this.showTestDataSource.asObservable();
|
||||
|
||||
constructor(private http: HttpClient) { }
|
||||
|
||||
public toggleShowTestData(newValue) {
|
||||
this.showTestDataSource.next(newValue);
|
||||
}
|
||||
|
||||
public getAvailableAppModules() {
|
||||
// returns all app modules and indicates the customers for which the user can use the modules
|
||||
return this.http.get<AvailableAppModule[]>('/api/v1/modules').pipe(catchError(this.handleError));
|
||||
|
@ -12,6 +12,7 @@ import { AppComponent } from './app.component';
|
||||
// Services
|
||||
import { AppModulesService } from './app-modules.service';
|
||||
import { LinkifyService } from './linkify.service';
|
||||
import { UberCacheService } from './uber-cache.service';
|
||||
|
||||
// Pipes
|
||||
import { LinkifyPipe } from './linkify.pipe';
|
||||
|
@ -0,0 +1,3 @@
|
||||
ul.nav a.test-active, ul.nav a.test-active > i {
|
||||
color: #ff9800
|
||||
}
|
||||
|
@ -56,11 +56,16 @@
|
||||
</div>
|
||||
<ul class="nav">
|
||||
<li routerLinkActive="active" *ngFor="let menuItem of menuItems" class="{{menuItem.class}} nav-item">
|
||||
<a class="nav-link" [routerLink]="[menuItem.path]">
|
||||
<a *ngIf="menuItem.key !== 'test-data'" class="nav-link" [routerLink]="[menuItem.path]">
|
||||
<i class="fas fa-{{menuItem.icon}}"></i>
|
||||
<!-- <i class="material-icons">{{menuItem.icon}}</i> -->
|
||||
<p>{{menuItem.title}}</p>
|
||||
</a>
|
||||
<a *ngIf="menuItem.key === 'test-data'" class="nav-link {{showTestData ? 'test-active' : ''}}" (click)="toggleTestMode()">
|
||||
<i class="fas fa-{{showTestData ? 'toggle-on' : 'toggle-off'}} {{showTestData ? 'test-active' : ''}}"></i>
|
||||
<!-- <i class="material-icons">{{menuItem.icon}}</i> -->
|
||||
<p>{{menuItem.title}}</p>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
import { AppModulesService } from '../../app-modules.service';
|
||||
import { ProfileService } from '../../profile.service';
|
||||
|
||||
import { AvailableAppModule } from '../../AppModulesTypes';
|
||||
import { LinkifyService } from '../../linkify.service';
|
||||
@ -11,6 +12,7 @@ declare interface RouteInfo {
|
||||
title: string;
|
||||
icon: string;
|
||||
class: string;
|
||||
key?: string;
|
||||
}
|
||||
export const ROUTES: RouteInfo[] = [
|
||||
{
|
||||
@ -55,6 +57,13 @@ export const ROUTES: RouteInfo[] = [
|
||||
icon: 'envelope',
|
||||
class: ''
|
||||
},
|
||||
{
|
||||
path: '/test-data',
|
||||
title: 'View test data',
|
||||
key: 'test-data',
|
||||
icon: 'toggle-on',
|
||||
class: ''
|
||||
},
|
||||
// { path: '/table-list', title: 'Table List', icon: 'content_paste', class: '' },
|
||||
// { path: '/typography', title: 'Typography', icon: 'library_books', class: '' },
|
||||
// { path: '/icons', title: 'Icons', icon: 'bubble_chart', class: '' },
|
||||
@ -69,12 +78,26 @@ export const ROUTES: RouteInfo[] = [
|
||||
styleUrls: ['./sidebar.component.css']
|
||||
})
|
||||
export class SidebarComponent implements OnInit {
|
||||
menuItems: any[];
|
||||
public menuItems: any[];
|
||||
public showTestData: boolean = false;
|
||||
|
||||
constructor(
|
||||
private appModulesService: AppModulesService,
|
||||
private linkify: LinkifyService,
|
||||
) { }
|
||||
private profileService: ProfileService
|
||||
) {
|
||||
appModulesService.showTestData.subscribe((newValue: boolean) => {
|
||||
this.showTestData = newValue;
|
||||
});
|
||||
|
||||
profileService.currentUser().subscribe((user) => {
|
||||
console.log(user);
|
||||
});
|
||||
|
||||
profileService.currentUser().subscribe((user) => {
|
||||
console.log(user);
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.menuItems = ROUTES.filter(menuItem => menuItem);
|
||||
@ -92,10 +115,15 @@ export class SidebarComponent implements OnInit {
|
||||
// // self.appModules = availableModules;
|
||||
// });
|
||||
}
|
||||
|
||||
isMobileMenu() {
|
||||
if ($(window).width() > 991) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
toggleTestMode() {
|
||||
this.appModulesService.toggleShowTestData(!this.showTestData);
|
||||
}
|
||||
}
|
||||
|
15
src/app/profile.service.spec.ts
Normal file
15
src/app/profile.service.spec.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { TestBed, inject } from '@angular/core/testing';
|
||||
|
||||
import { ProfileService } from './profile.service';
|
||||
|
||||
describe('ProfileService', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [ProfileService]
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created', inject([ProfileService], (service: ProfileService) => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
});
|
48
src/app/profile.service.ts
Normal file
48
src/app/profile.service.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable, throwError } from 'rxjs';
|
||||
import { map, catchError } from 'rxjs/operators';
|
||||
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
|
||||
|
||||
import { UberCacheService } from './uber-cache.service';
|
||||
|
||||
import { User } from './User';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ProfileService {
|
||||
|
||||
constructor(
|
||||
private http: HttpClient,
|
||||
private uberCache: UberCacheService
|
||||
) { }
|
||||
|
||||
private requestCurrentUser(): Observable<User> {
|
||||
return this.http.get<UserResponse>('/api/v1/profile/me').pipe(catchError(this.handleError)).pipe(map((res: UserResponse) => res.user));
|
||||
}
|
||||
|
||||
public currentUser(): Observable<User> {
|
||||
return this.uberCache.getRequest(this.requestCurrentUser.bind(this), `/api/v1/profile/events`, 'userUpdated');
|
||||
}
|
||||
|
||||
private handleError(error: HttpErrorResponse) {
|
||||
if (error.error instanceof ErrorEvent) {
|
||||
// client or network error
|
||||
console.error('An error occurred:', error.error.message);
|
||||
} else {
|
||||
// backend returned an error
|
||||
console.error(
|
||||
`Backend returned code ${error.status}, ` +
|
||||
`body was: ${error.error}`
|
||||
);
|
||||
}
|
||||
return throwError(
|
||||
`An eror occurred`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
interface UserResponse {
|
||||
status: string;
|
||||
user: User
|
||||
}
|
15
src/app/uber-cache.service.spec.ts
Normal file
15
src/app/uber-cache.service.spec.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { TestBed, inject } from '@angular/core/testing';
|
||||
|
||||
import { UberCacheService } from './uber-cache.service';
|
||||
|
||||
describe('UberCacheService', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [UberCacheService]
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created', inject([UberCacheService], (service: UberCacheService) => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
});
|
63
src/app/uber-cache.service.ts
Normal file
63
src/app/uber-cache.service.ts
Normal file
@ -0,0 +1,63 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable, Subject, merge } from 'rxjs';
|
||||
import { switchMap, shareReplay } from 'rxjs/operators';
|
||||
|
||||
import { NativeEventSource, EventSourcePolyfill } from 'event-source-polyfill';
|
||||
const EventSource = NativeEventSource || EventSourcePolyfill;
|
||||
// OR: may also need to set as global property
|
||||
// global.EventSource = NativeEventSource || EventSourcePolyfill;
|
||||
|
||||
const CACHE_SIZE = 1;
|
||||
|
||||
// Creates a cache of requests that are automatically busted based on server sent events
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class UberCacheService {
|
||||
private cache$: CacheStore;
|
||||
|
||||
constructor() {
|
||||
this.cache$ = {};
|
||||
}
|
||||
|
||||
public getRequest<T>(request: ()=>Observable<T>, eventUrl: string, eventName: string): Observable<T> {
|
||||
if (!this.cache$[eventUrl]) {
|
||||
const updateStream = this.createUpdateStream(eventUrl, eventName);
|
||||
const initialCurrentUser$ = request();
|
||||
const reload$ = updateStream.pipe(switchMap(() => request()));
|
||||
const newCache = merge(initialCurrentUser$, reload$).pipe(shareReplay(CACHE_SIZE));
|
||||
this.cache$[eventUrl] = {
|
||||
cache: newCache,
|
||||
stream: updateStream
|
||||
};
|
||||
}
|
||||
return this.cache$[eventUrl].cache;
|
||||
}
|
||||
|
||||
private createUpdateStream(eventUrl: string, eventName: string) {
|
||||
const eventStream = new Subject<void>();
|
||||
const es = new EventSource(eventUrl);
|
||||
es.addEventListener(eventName, (evt: StreamEvent) => {
|
||||
console.log(evt.data);
|
||||
eventStream.next();
|
||||
});
|
||||
return eventStream;
|
||||
}
|
||||
}
|
||||
|
||||
interface StreamEvent {
|
||||
data?: string;
|
||||
event?: string;
|
||||
id?: string;
|
||||
retry?: number;
|
||||
}
|
||||
|
||||
interface CacheStore {
|
||||
[name: string]: CacheTools;
|
||||
}
|
||||
|
||||
interface CacheTools {
|
||||
cache: Observable<any>;
|
||||
stream: Subject<void>;
|
||||
}
|
@ -66,3 +66,4 @@ import 'zone.js/dist/zone'; // Included with Angular CLI.
|
||||
* Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10
|
||||
*/
|
||||
// import 'intl'; // Run `npm install --save intl`.
|
||||
import 'event-source-polyfill';
|
||||
|
@ -1690,7 +1690,7 @@ class-utils@^0.3.5:
|
||||
isobject "^3.0.0"
|
||||
static-extend "^0.1.1"
|
||||
|
||||
classlist.js@1.1.20150312:
|
||||
classlist.js@^1.1.20150312:
|
||||
version "1.1.20150312"
|
||||
resolved "https://registry.yarnpkg.com/classlist.js/-/classlist.js-1.1.20150312.tgz#1d70842f7022f08d9ac086ce69e5b250f2c57789"
|
||||
integrity sha1-HXCEL3Ai8I2awIbOaeWyUPLFd4k=
|
||||
@ -2789,6 +2789,11 @@ etag@~1.8.1:
|
||||
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
|
||||
integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
|
||||
|
||||
event-source-polyfill@^1.0.5:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/event-source-polyfill/-/event-source-polyfill-1.0.5.tgz#b34be2740a685a8dc65ae750065fc983538ffcfe"
|
||||
integrity sha512-PdStgZ3+G2o2gjqsBYbV4931ByVmwLwSrX7mFgawCL+9I1npo9dwAQTnWtNWXe5IY2P8+AbbPteeOueiEtRCUA==
|
||||
|
||||
event-target-shim@^5.0.0:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789"
|
||||
@ -8545,7 +8550,7 @@ wbuf@^1.1.0, wbuf@^1.7.3:
|
||||
dependencies:
|
||||
minimalistic-assert "^1.0.0"
|
||||
|
||||
web-animations-js@2.3.1:
|
||||
web-animations-js@^2.3.1:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/web-animations-js/-/web-animations-js-2.3.1.tgz#3a6d9bc15196377a90f8e2803fa5262165b04510"
|
||||
integrity sha1-Om2bwVGWN3qQ+OKAP6UmIWWwRRA=
|
||||
|
Reference in New Issue
Block a user