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-material-design": "4.1.1",
|
||||||
"bootstrap-notify": "3.1.3",
|
"bootstrap-notify": "3.1.3",
|
||||||
"chartist": "0.11.0",
|
"chartist": "0.11.0",
|
||||||
"classlist.js": "1.1.20150312",
|
"classlist.js": "^1.1.20150312",
|
||||||
"core-js": "2.4.1",
|
"core-js": "2.4.1",
|
||||||
|
"event-source-polyfill": "^1.0.5",
|
||||||
"express": "4.16.3",
|
"express": "4.16.3",
|
||||||
"googleapis": "28.1.0",
|
"googleapis": "28.1.0",
|
||||||
"hammerjs": "2.0.8",
|
"hammerjs": "2.0.8",
|
||||||
@ -47,10 +48,11 @@
|
|||||||
"popper.js": "1.14.3",
|
"popper.js": "1.14.3",
|
||||||
"rxjs": "6.3.3",
|
"rxjs": "6.3.3",
|
||||||
"rxjs-compat": "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"
|
"zone.js": "0.8.26"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@angular-devkit/build-angular": "~0.6.3",
|
||||||
"@angular/cli": "6.0.3",
|
"@angular/cli": "6.0.3",
|
||||||
"@angular/compiler-cli": "7.0.2",
|
"@angular/compiler-cli": "7.0.2",
|
||||||
"@angular/language-service": "7.0.2",
|
"@angular/language-service": "7.0.2",
|
||||||
@ -71,7 +73,6 @@
|
|||||||
"protractor": "5.3.1",
|
"protractor": "5.3.1",
|
||||||
"ts-node": "5.0.1",
|
"ts-node": "5.0.1",
|
||||||
"tslint": "5.9.1",
|
"tslint": "5.9.1",
|
||||||
"typescript": "3.1.6",
|
"typescript": "3.1.6"
|
||||||
"@angular-devkit/build-angular": "~0.6.3"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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 { Observable, throwError } from 'rxjs';
|
||||||
import { catchError } from 'rxjs/operators';
|
import { catchError } from 'rxjs/operators';
|
||||||
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
|
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
|
||||||
|
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
|
||||||
|
|
||||||
import { AvailableAppModule } from './AppModulesTypes';
|
import { AvailableAppModule } from './AppModulesTypes';
|
||||||
|
|
||||||
@ -9,9 +10,16 @@ import { AvailableAppModule } from './AppModulesTypes';
|
|||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class AppModulesService {
|
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) { }
|
constructor(private http: HttpClient) { }
|
||||||
|
|
||||||
|
public toggleShowTestData(newValue) {
|
||||||
|
this.showTestDataSource.next(newValue);
|
||||||
|
}
|
||||||
|
|
||||||
public getAvailableAppModules() {
|
public getAvailableAppModules() {
|
||||||
// returns all app modules and indicates the customers for which the user can use the modules
|
// 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));
|
return this.http.get<AvailableAppModule[]>('/api/v1/modules').pipe(catchError(this.handleError));
|
||||||
|
@ -12,6 +12,7 @@ import { AppComponent } from './app.component';
|
|||||||
// Services
|
// Services
|
||||||
import { AppModulesService } from './app-modules.service';
|
import { AppModulesService } from './app-modules.service';
|
||||||
import { LinkifyService } from './linkify.service';
|
import { LinkifyService } from './linkify.service';
|
||||||
|
import { UberCacheService } from './uber-cache.service';
|
||||||
|
|
||||||
// Pipes
|
// Pipes
|
||||||
import { LinkifyPipe } from './linkify.pipe';
|
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>
|
</div>
|
||||||
<ul class="nav">
|
<ul class="nav">
|
||||||
<li routerLinkActive="active" *ngFor="let menuItem of menuItems" class="{{menuItem.class}} nav-item">
|
<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="fas fa-{{menuItem.icon}}"></i>
|
||||||
<!-- <i class="material-icons">{{menuItem.icon}}</i> -->
|
<!-- <i class="material-icons">{{menuItem.icon}}</i> -->
|
||||||
<p>{{menuItem.title}}</p>
|
<p>{{menuItem.title}}</p>
|
||||||
</a>
|
</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>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
|
||||||
import { AppModulesService } from '../../app-modules.service';
|
import { AppModulesService } from '../../app-modules.service';
|
||||||
|
import { ProfileService } from '../../profile.service';
|
||||||
|
|
||||||
import { AvailableAppModule } from '../../AppModulesTypes';
|
import { AvailableAppModule } from '../../AppModulesTypes';
|
||||||
import { LinkifyService } from '../../linkify.service';
|
import { LinkifyService } from '../../linkify.service';
|
||||||
@ -11,6 +12,7 @@ declare interface RouteInfo {
|
|||||||
title: string;
|
title: string;
|
||||||
icon: string;
|
icon: string;
|
||||||
class: string;
|
class: string;
|
||||||
|
key?: string;
|
||||||
}
|
}
|
||||||
export const ROUTES: RouteInfo[] = [
|
export const ROUTES: RouteInfo[] = [
|
||||||
{
|
{
|
||||||
@ -55,6 +57,13 @@ export const ROUTES: RouteInfo[] = [
|
|||||||
icon: 'envelope',
|
icon: 'envelope',
|
||||||
class: ''
|
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: '/table-list', title: 'Table List', icon: 'content_paste', class: '' },
|
||||||
// { path: '/typography', title: 'Typography', icon: 'library_books', class: '' },
|
// { path: '/typography', title: 'Typography', icon: 'library_books', class: '' },
|
||||||
// { path: '/icons', title: 'Icons', icon: 'bubble_chart', class: '' },
|
// { path: '/icons', title: 'Icons', icon: 'bubble_chart', class: '' },
|
||||||
@ -69,12 +78,26 @@ export const ROUTES: RouteInfo[] = [
|
|||||||
styleUrls: ['./sidebar.component.css']
|
styleUrls: ['./sidebar.component.css']
|
||||||
})
|
})
|
||||||
export class SidebarComponent implements OnInit {
|
export class SidebarComponent implements OnInit {
|
||||||
menuItems: any[];
|
public menuItems: any[];
|
||||||
|
public showTestData: boolean = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private appModulesService: AppModulesService,
|
private appModulesService: AppModulesService,
|
||||||
private linkify: LinkifyService,
|
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() {
|
ngOnInit() {
|
||||||
this.menuItems = ROUTES.filter(menuItem => menuItem);
|
this.menuItems = ROUTES.filter(menuItem => menuItem);
|
||||||
@ -92,10 +115,15 @@ export class SidebarComponent implements OnInit {
|
|||||||
// // self.appModules = availableModules;
|
// // self.appModules = availableModules;
|
||||||
// });
|
// });
|
||||||
}
|
}
|
||||||
|
|
||||||
isMobileMenu() {
|
isMobileMenu() {
|
||||||
if ($(window).width() > 991) {
|
if ($(window).width() > 991) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
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
|
* Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10
|
||||||
*/
|
*/
|
||||||
// import 'intl'; // Run `npm install --save intl`.
|
// import 'intl'; // Run `npm install --save intl`.
|
||||||
|
import 'event-source-polyfill';
|
||||||
|
@ -1690,7 +1690,7 @@ class-utils@^0.3.5:
|
|||||||
isobject "^3.0.0"
|
isobject "^3.0.0"
|
||||||
static-extend "^0.1.1"
|
static-extend "^0.1.1"
|
||||||
|
|
||||||
classlist.js@1.1.20150312:
|
classlist.js@^1.1.20150312:
|
||||||
version "1.1.20150312"
|
version "1.1.20150312"
|
||||||
resolved "https://registry.yarnpkg.com/classlist.js/-/classlist.js-1.1.20150312.tgz#1d70842f7022f08d9ac086ce69e5b250f2c57789"
|
resolved "https://registry.yarnpkg.com/classlist.js/-/classlist.js-1.1.20150312.tgz#1d70842f7022f08d9ac086ce69e5b250f2c57789"
|
||||||
integrity sha1-HXCEL3Ai8I2awIbOaeWyUPLFd4k=
|
integrity sha1-HXCEL3Ai8I2awIbOaeWyUPLFd4k=
|
||||||
@ -2789,6 +2789,11 @@ etag@~1.8.1:
|
|||||||
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
|
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
|
||||||
integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
|
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:
|
event-target-shim@^5.0.0:
|
||||||
version "5.0.1"
|
version "5.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789"
|
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:
|
dependencies:
|
||||||
minimalistic-assert "^1.0.0"
|
minimalistic-assert "^1.0.0"
|
||||||
|
|
||||||
web-animations-js@2.3.1:
|
web-animations-js@^2.3.1:
|
||||||
version "2.3.1"
|
version "2.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/web-animations-js/-/web-animations-js-2.3.1.tgz#3a6d9bc15196377a90f8e2803fa5262165b04510"
|
resolved "https://registry.yarnpkg.com/web-animations-js/-/web-animations-js-2.3.1.tgz#3a6d9bc15196377a90f8e2803fa5262165b04510"
|
||||||
integrity sha1-Om2bwVGWN3qQ+OKAP6UmIWWwRRA=
|
integrity sha1-Om2bwVGWN3qQ+OKAP6UmIWWwRRA=
|
||||||
|
Reference in New Issue
Block a user