add uber cache and some streams for test data

This commit is contained in:
Mason Payne
2019-03-23 02:34:47 -06:00
parent c95f5614b5
commit 17ddad6a0f
13 changed files with 257 additions and 21 deletions

View File

@ -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
View 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
}

View File

@ -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));

View File

@ -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';

View File

@ -0,0 +1,3 @@
ul.nav a.test-active, ul.nav a.test-active > i {
color: #ff9800
}

View File

@ -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>

View File

@ -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[] = [
{
@ -22,45 +24,52 @@ export const ROUTES: RouteInfo[] = [
{
path: '/user-profile',
title: 'User Profile',
icon:'user-alt',
icon: 'user-alt',
class: ''
},
{
path: '/authentication',
title: 'Authentication',
icon:'key',
icon: 'key',
class: ''
},
{
path: '/authorization',
title: 'Authorization',
icon:'shield-alt',
icon: 'shield-alt',
class: ''
},
{
path: '/feature-switches',
title: 'Feature Switches',
icon:'toggle-on',
icon: 'toggle-on',
class: ''
},
{
path: '/short-links',
title: 'Short Links',
icon:'link',
icon: 'link',
class: ''
},
{
path: '/email',
title: 'Email',
icon:'envelope',
icon: 'envelope',
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: '' },
// { path: '/maps', title: 'Maps', icon:'location_on', class: '' },
// { path: '/notifications', title: 'Notifications', icon:'notifications', class: '' },
// { path: '/upgrade', title: 'Upgrade to PRO', icon:'unarchive', class: 'active-pro' },
{
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: '' },
// { path: '/maps', title: 'Maps', icon: 'location_on', class: '' },
// { path: '/notifications', title: 'Notifications', icon: 'notifications', class: '' },
// { path: '/upgrade', title: 'Upgrade to PRO', icon: 'unarchive', class: 'active-pro' },
];
@Component({
@ -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);
}
}

View 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();
}));
});

View 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
}

View 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();
}));
});

View 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>;
}

View File

@ -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';

View File

@ -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=