Verified Commit 713a0b37 authored by Raphael Ochsenbein's avatar Raphael Ochsenbein
Browse files

Added alternative toast variant, to be combined / included; Added...

Added alternative toast variant, to be combined / included; Added ngrx-store-devtools; display ID token in dom, remove console.log
parent 3d52c840
......@@ -743,6 +743,11 @@
"resolved": "https://registry.npmjs.org/@ngrx/store/-/store-7.2.0.tgz",
"integrity": "sha512-E9c0cDot0HeE0mXyeqw18SwmJ2+eKnA5mMMfwvoskpMInCYGI2pq1i6/lCVQ2wrEHSH+KvObK4PQbepcA9vP+w=="
},
"@ngrx/store-devtools": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/@ngrx/store-devtools/-/store-devtools-7.2.0.tgz",
"integrity": "sha512-t+8K1IG8+MvFqLIuRSM+ZE1EkZIuUExJ0JsqZR4r4K3MRPRoGy1ZqlStBWYaYLumEToesiCOGxuJYQ4zyVwlZg=="
},
"@ngtools/webpack": {
"version": "7.3.2",
"resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-7.3.2.tgz",
......
......@@ -13,3 +13,21 @@
<button [ibmButton]="'primary'" [size]="'normal'" (click)="check2FA()">Check 2FA</button>
<button [ibmButton]="'danger--primary'" [size]="'normal'" (click)="loginPopup2fA()">Login Request 2FA</button>
<hr/>
<ng-container *ngIf="error && (error.signInError || error.silentRenewError)">
<h2>Error</h2>
<ibm-code-snippet display="multi">{{ error | json }}</ibm-code-snippet>
<hr/>
</ng-container>
<ng-container *ngIf="identity">
<h2>Identity</h2>
<h3>Your Profile</h3>
<ibm-code-snippet display="multi">{{ identity.profile | json }}</ibm-code-snippet>
<h3>Scopes</h3>
<ibm-code-snippet display="multi">{{ identity.scope }}</ibm-code-snippet>
<h3>ID Token</h3>
<ibm-code-snippet display="multi">{{ identity.id_token }}</ibm-code-snippet>
<h3>Access Token</h3>
<ibm-code-snippet display="multi">{{ identity.access_token }}</ibm-code-snippet>
<hr/>
</ng-container>
h1,
h2 {
color: whitesmoke;
h2,
h3 {
margin-top: 1em;
}
button {
margin-right: 4px;
}
......@@ -6,6 +6,8 @@ import {catchError, mergeMap, take} from 'rxjs/operators';
import { environment } from '.././environments/environment';
import {HttpClient} from '@angular/common/http';
import {isArray} from 'util';
import {User as OidcUser} from 'oidc-client';
import {ErrorState} from 'ng-oidc-client/lib/reducers/oidc.reducer';
@Component({
selector: 'app-root',
......@@ -17,6 +19,9 @@ export class AppComponent implements OnDestroy {
private audience = { audience: this.audienceApi };
title = 'openid-connect-playground';
identity: OidcUser;
error: ErrorState;
loadingSub: Subscription;
expiringSub: Subscription;
expiredSub: Subscription;
......@@ -30,34 +35,40 @@ export class AppComponent implements OnDestroy {
private http: HttpClient,
) {
this.loadingSub = this.oidcFacade.loading$.subscribe((data) => {
console.log('Loading', data);
});
this.expiringSub = this.oidcFacade.expiring$.subscribe((data) => {
console.log('Expiring', data);
if (data) {
this.toast.show({
type: 'info',
title: 'Login Expiring',
caption: 'Please refresh the token',
});
}
});
this.expiredSub = this.oidcFacade.expired$.subscribe((data) => {
console.log('Expired', data);
});
this.loggedInSub = this.oidcFacade.loggedIn$.subscribe((data) => {
console.log('Logged In', data);
if (data) {
this.toast.show({
type: 'error',
title: 'Login Expired',
caption: 'You need to login again',
});
}
});
this.errorSub = this.oidcFacade.errors$.subscribe((data) => {
console.log('Error', data);
this.toast.show({
type: 'error',
title: 'Error',
caption: `${JSON.stringify(data)}`,
});
this.error = data;
if (data && (data.silentRenewError || data.signInError)) {
this.toast.show({
type: 'error',
title: 'Error',
caption: `${JSON.stringify(data)}`,
});
}
});
this.identitySub = this.oidcFacade.identity$.subscribe((data) => {
console.log('Identity', data);
this.identity = data;
});
}
......
......@@ -2,22 +2,21 @@ import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import {ActionReducerMap, StoreModule} from '@ngrx/store';
import {Log, WebStorageStateStore} from 'oidc-client';
import {EffectsModule} from '@ngrx/effects';
import {Config as OidcConfig, NgOidcClientModule} from 'ng-oidc-client';
import {routerReducer, RouterReducerState} from '@ngrx/router-store';
import {OidcGuardService} from './oidc-guard.service';
import {HTTP_INTERCEPTORS, HttpClientModule} from '@angular/common/http';
import {OidcInterceptorService} from './oidc-interceptor.service';
import {OidcEffectsService} from './oidc-effects.service';
import {metaReducers} from './logout.metareducer';
import {ButtonModule, NotificationModule, NotificationService} from 'carbon-components-angular';
import {ToastModule} from './toast';
import {FormsModule} from '@angular/forms';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {environment} from '../environments/environment';
import {ConfiguredOidcModuleModule} from './configured-oidc.module';
import { ActionReducerMap, StoreModule } from '@ngrx/store';
import { routerReducer, RouterReducerState } from '@ngrx/router-store';
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
import { EffectsModule } from '@ngrx/effects';
import { OidcGuardService } from './oidc-guard.service';
import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';
import { OidcInterceptorService } from './oidc-interceptor.service';
import { OidcEffectsService } from './oidc-effects.service';
import { metaReducers } from './logout.metareducer';
import { ButtonModule, CodeSnippetModule } from 'carbon-components-angular';
import { ToastModule } from './toast';
import { FormsModule } from '@angular/forms';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { environment } from '../environments/environment';
import { ConfiguredOidcModuleModule } from './configured-oidc.module';
// Setup done according to https://www.npmjs.com/package/ng-oidc-client
......@@ -29,7 +28,6 @@ export const rootStore: ActionReducerMap<State> = {
router: routerReducer
};
@NgModule({
declarations: [
AppComponent,
......@@ -40,8 +38,10 @@ export const rootStore: ActionReducerMap<State> = {
BrowserAnimationsModule,
HttpClientModule,
ButtonModule,
CodeSnippetModule,
ToastModule.forRoot(),
StoreModule.forRoot(rootStore, { metaReducers}),
StoreDevtoolsModule.instrument(), // could be like configured oidc module to only import in dev
EffectsModule.forRoot([OidcEffectsService]),
ConfiguredOidcModuleModule.forRoot(environment),
],
......
import {BehaviorSubject} from 'rxjs';
import {ToastDataService} from './toast-data.service';
import {ToastRef} from './toast-ref';
describe(`ToastDataService`, () => {
let service: ToastDataService;
let toastsSubject: BehaviorSubject<ToastRef[]>;
const ref: ToastRef = {
title: 'Title',
message: 'message',
timeOnScreen: 500,
fadeTime: 100,
cssClass: 'class',
style: '',
hide: () => {},
};
beforeEach(() => {
service = new ToastDataService();
toastsSubject = (service as any).toastsSubject;
});
/*
it(`should be instantiable`, () => {
expect(service).toEqual(jasmine.any(ToastDataService));
});
*/
describe(`constructor()`, () => {
it(`should start with an empty list of toasts`, () => {
expect(service.toasts).toEqual([]);
});
}); // End describe: constructor()
describe(`get toasts$`, () => {
it(`adding or removing toasts should trigger a change`, () => {
let toasts: ToastRef[] = null;
service.toasts$.subscribe(t => toasts = t);
expect(toasts).toEqual([]);
service.add(ref);
expect(toasts).toEqual([ref]);
service.add(ref);
expect(toasts).toEqual([ref, ref]);
});
}); // End describe: adding or removing toasts should trigger a change
describe(`get toasts`, () => {
it(`should return the current toast values`, () => {
toastsSubject.next([ref]);
expect(service.toasts).toEqual([ref]);
});
}); // End describe: get toasts
describe(`set toasts`, () => {
it(`should set the toasts`, () => {
service.toasts = [ref];
expect(toastsSubject.getValue()).toEqual([ref]);
});
}); // End describe: set toasts
describe(`add()`, () => {
it(`should add the ref to the list`, () => {
service.add(ref);
expect(service.toasts).toEqual([ref]);
});
}); // End describe: add()
describe(`remove()`, () => {
it(`should remove the toast`, () => {
service.toasts = [ref];
service.remove(ref);
expect(service.toasts).toEqual([]);
});
}); // End describe: remove()
}); // End describe: ToastDataService
import {Injectable} from '@angular/core';
import {ToastRef} from './toast-ref';
import {BehaviorSubject, Observable} from 'rxjs';
@Injectable()
export class ToastDataService {
get toasts$(): Observable<ToastRef[]> {
return this.toastsSubject.asObservable();
}
get toasts(): ToastRef[] {
return this.toastsSubject.getValue();
}
set toasts(value: ToastRef[]) {
this.toastsSubject.next(value);
}
private toastsSubject = new BehaviorSubject<ToastRef[]>([]);
add(ref: ToastRef) {
this.toasts = [...this.toasts, ref];
return this.toasts;
}
remove(ref: ToastRef) {
this.toasts = this.toasts.filter(r => r !== ref);
return this.toasts;
}
}
export type ToastStyle = '' | 'success' | 'info' | 'warn' | 'error';
export const ToastStyles = {
None: '' as ToastStyle,
Success: 'success' as ToastStyle,
Info: 'info' as ToastStyle,
Warn: 'warn' as ToastStyle,
Error: 'error' as ToastStyle
};
export interface ToastRef {
title: string;
message: string;
timeOnScreen: number;
fadeTime: number;
cssClass: string;
style?: ToastStyle;
hide: () => any;
}
import {Component, EventEmitter, Input, OnChanges, Output} from '@angular/core';
import {OnInit} from '@angular/core';
import {HostBinding} from '@angular/core';
import {ToastStyle} from './toast-ref';
@Component({
selector: 'app-toast-alt',
template: `
<div class="card-body">
<div class="split">
<div class="title-and-message">
<div class="title">{{ title }}</div>
<div class="message">{{ message }}</div>
</div>
<div class="close">
<button type="button" (click)="onClose()">X</button>
</div>
</div>
</div>
`
})
export class ToastComponent implements OnInit, OnChanges {
@Input()
title: string;
@Input()
message: string;
@Input()
style: ToastStyle;
@Input()
cssClass: string;
@HostBinding('class')
cssKlass: string;
@Output()
close = new EventEmitter<any>();
private get calculatedCssClass() {
return `card ${ this.cssClass } ${ this.style }`;
}
ngOnInit() {
this.cssKlass = this.calculatedCssClass;
}
ngOnChanges() {
this.cssKlass = this.calculatedCssClass;
}
onClose() {
this.close.emit();
}
}
import {NgModule} from '@angular/core';
import {ToastsComponent} from './toasts.component';
import {ToastService} from './toast.service';
import {CommonModule} from '@angular/common';
import {ToastDataService} from './toast-data.service';
import {ToastComponent} from './toast.component';
import {OverlayModule} from '@angular/cdk/overlay';
const declareAndExport = [
ToastsComponent,
];
@NgModule({
imports: [
CommonModule,
OverlayModule,
],
providers: [
ToastService,
ToastDataService,
],
declarations: [
...declareAndExport,
ToastComponent,
],
entryComponents: [
ToastsComponent,
],
exports: [
...declareAndExport,
],
})
export class ToastModule {
}
import {Overlay} from '@angular/cdk/overlay';
import {fakeAsync, tick} from '@angular/core/testing';
import {ToastService} from './toast.service';
import {ToastDataService} from './toast-data.service';
export class PositionStrategyMock {
global = () => this;
right = () => this;
top = () => this;
}
describe(`ToastService`, () => {
let service: ToastService;
let data: ToastDataService;
let overlay: Overlay;
let overlayStatus;
beforeEach(() => {
data = new ToastDataService();
overlay = {
position: () => new PositionStrategyMock(),
scrollStrategies: {
block: () => 'block',
reposition: () => 'reposition'
},
create: jasmine.createSpy('create').and.callFake(() => ({
attach: () => overlayStatus = 'attached',
dispose: () => overlayStatus = 'disposed'
})),
} as any;
service = new ToastService(overlay, data);
});
describe(`constructor()`, () => {
it(`should be instantiable`, () => {
expect(service).toEqual(jasmine.any(ToastService));
});
}); // End describe: constructor()
describe(`show()`, () => {
it(`Should properly setup the toast`, fakeAsync(() => {
expect(overlay.create).toHaveBeenCalledTimes(0);
service.show('Title 1', 'Message 1');
service.show('Title 2', 'Message 2');
// Overlay should only be created once
expect(data.toasts.length).toEqual(2);
expect(overlay.create).toHaveBeenCalledTimes(1);
expect(data.toasts[0].cssClass).toEqual('show-toast');
// Wait for fade (500) and duration (5000)
tick(5500);
expect(data.toasts[0].cssClass).toEqual('hide-toast');
tick(500);
expect(data.toasts[0].cssClass).toEqual('shrink-toast');
tick(500);
expect(data.toasts.length).toEqual(0);
}));
it(`should use the style and timeOnScreen values if provided`, fakeAsync(() => {
service.show('Title 1', 'Message 1', 'warn', 100);
expect(data.toasts.length).toEqual(1);
expect(data.toasts[0].style).toEqual('warn');
tick(500);
expect(data.toasts[0].cssClass).toEqual('show-toast');
tick(100);
expect(data.toasts[0].cssClass).toEqual('hide-toast');
tick(500);
expect(data.toasts[0].cssClass).toEqual('shrink-toast');
tick(500);
expect(data.toasts.length).toEqual(0);
}));
}); // End describe: show()
}); // End describe: ToastService
import {Injectable} from '@angular/core';
import {Overlay} from '@angular/cdk/overlay';
import {MonoTypeOperatorFunction, throwError, timer} from 'rxjs';
import {ComponentPortal} from '@angular/cdk/portal';
import {ToastsComponent} from './toasts.component';
import {ToastDataService} from './toast-data.service';
import {ToastRef, ToastStyle, ToastStyles} from './toast-ref';
import {catchError, take} from 'rxjs/operators';
const fadeTime = 500;
@Injectable()
export class ToastService {
private positionStrategy;
private scrollStrategy;
private ref;
constructor(
private overlay: Overlay,
private data: ToastDataService
) {
this.positionStrategy = overlay.position()
.global()
.right('23px')
.top('76px');
this.scrollStrategy = this.overlay.scrollStrategies.reposition();
}
show(title: string, message: string, style: ToastStyle = '', timeOnScreen = 5000) {
let hideStarted = false;
const ref: ToastRef = {
title, message, timeOnScreen, fadeTime, cssClass: 'show-toast', style, hide: () => {
if (hideStarted) {
return;
}
hideStarted = true;
ref.cssClass = 'hide-toast';
timer(ref.fadeTime, ref.fadeTime)
.pipe(take(2))
.subscribe(index => {
if (index === 0) {
ref.cssClass = 'shrink-toast';
} else {
this.hide(ref);
}
});
}
};
if (this.data.add(ref).length === 1) {
this.showOverlay();
}
timer(ref.fadeTime + ref.timeOnScreen, ref.fadeTime)
.pipe(take(1))
.subscribe(() => ref.hide());
}
catchAndNotifyGenericError<T>(
title = 'An error has occurred',
message = '',
callback: () => any = () => undefined
): MonoTypeOperatorFunction<T> {
return catchError(error => {
this.show(title, message, ToastStyles.Error);
if (callback) {
callback();
}
return throwError(error);
});
}
catchAndNotifyDataLoadingError<T>(callback?: () => any): MonoTypeOperatorFunction<T> {
return this.catchAndNotifyGenericError<T>('An error has occurred', 'Data could not be loaded', callback);
}
catchAndNotifyActionError<T>(action?: string, callback?: () => any): MonoTypeOperatorFunction<T> {
return this.catchAndNotifyGenericError<T>(
'An error has occurred',
action
? `Could not ${action}`
: 'The action could not be completed.',
callback
);
}
private hide(ref: ToastRef) {