diff --git a/docs/TIMEOUT_WARNING.md b/docs/TIMEOUT_WARNING.md
new file mode 100644
index 0000000000..ebc707a422
--- /dev/null
+++ b/docs/TIMEOUT_WARNING.md
@@ -0,0 +1,42 @@
+# Session Timeout Warning configuration
+
+Stark provides a nice feature when it comes to session expiration handling: in case the user's session is about to end due
+to inactivity (the user is idle for some time) and you want to warn him about this, a dialog will be displayed.
+
+This is what the `SessionTimeoutWarningDialogComponent` does and also asks the user if its session should be kept alive.
+
+This warning dialog is displayed by an NGRX Effect that triggers when a `StarkSessionActionTypes.SESSION_TIMEOUT_COUNTDOWN_START` action
+is dispatched (by the Session service when the user becomes idle).
+
+To use this feature, you'll have to modify the `app.module.ts` file of your application
+
+## app.module.ts
+
+You'll have to import the StarkSessionUiModule like follow:
+
+```
+import {StarkSessionUiModule} from "@nationalbankbelgium/stark-ui";
+
+@NgModule({
+ imports: [
+ StarkSessionUiModule.forRoot(),
+ ...
+ ]
+})
+```
+
+To indicate that you don't wish to display such warning dialog, just set `timeoutWarningDialogDisabled` value to true.
+This option is false by default.
+
+```
+import {StarkSessionUiModule} from "@nationalbankbelgium/stark-ui";
+
+@NgModule({
+ imports: [
+ StarkSessionUiModule.forRoot({
+ timeoutWarningDialogDisabled: true
+ }),
+ ...
+ ]
+})
+```
diff --git a/packages/stark-build/config/webpack.dev.js b/packages/stark-build/config/webpack.dev.js
index 3d4c8ad8c5..25c4058faf 100644
--- a/packages/stark-build/config/webpack.dev.js
+++ b/packages/stark-build/config/webpack.dev.js
@@ -47,7 +47,7 @@ module.exports = function(env) {
// "default-src 'self'", // FIXME: enable as soon as the issue is fixed in Angular (https://github.com/angular/angular-cli/issues/6872 )
"child-src 'self'",
"connect-src 'self' ws://" + METADATA.HOST + ":" + METADATA.PORT + " " + webpackCustomConfig["cspConnectSrc"], // ws://HOST:PORT" is due to Webpack
- `font-src 'self' ${webpackCustomConfig["cspFontSrc"] || ''}`,
+ `font-src 'self' ${webpackCustomConfig["cspFontSrc"] || ""}`,
"form-action 'self' " + webpackCustomConfig["cspFormAction"],
"frame-src 'self'", // deprecated. Use child-src instead. Used here because child-src is not yet supported by Firefox. Remove as soon as it is fully supported
"frame-ancestors 'none'", // the app will not be allowed to be embedded in an iframe (roughly equivalent to X-Frame-Options: DENY)
@@ -279,14 +279,14 @@ module.exports = function(env) {
*/
...(MONITOR
? [
- new WebpackMonitor({
- capture: true, // -> default 'true'
- target: helpers.root("reports/webpack-monitor/stats.json"), // default -> '../monitor/stats.json'
- launch: true, // -> default 'false'
- port: 3030, // default -> 8081
- excludeSourceMaps: true // default 'true'
- })
- ]
+ new WebpackMonitor({
+ capture: true, // -> default 'true'
+ target: helpers.root("reports/webpack-monitor/stats.json"), // default -> '../monitor/stats.json'
+ launch: true, // -> default 'false'
+ port: 3030, // default -> 8081
+ excludeSourceMaps: true // default 'true'
+ })
+ ]
: [])
],
diff --git a/packages/stark-ui/assets/styles/_media-queries.scss b/packages/stark-ui/assets/styles/_media-queries.scss
index d9b86d91f9..f3ee2456a0 100644
--- a/packages/stark-ui/assets/styles/_media-queries.scss
+++ b/packages/stark-ui/assets/styles/_media-queries.scss
@@ -7,5 +7,5 @@ $mobile-only-query: "(max-width: 599px)";
$mobile-only-screen-query: "screen and (max-width: 599px)";
$tablet-only-query: "(min-width: 600px) and (max-width: 959px)";
$tablet-only-screen-query: "screen and (min-width: 600px) and (max-width: 959px)";
-$handset-toc-query-screen:"(min-width: 0px) and (max-width: 839px)";
-$tablet-toc-query-screen:"(min-width: 840px) and (max-width: 1279px)";
+$handset-toc-query-screen: "(min-width: 0px) and (max-width: 839px)";
+$tablet-toc-query-screen: "(min-width: 840px) and (max-width: 1279px)";
diff --git a/packages/stark-ui/src/modules/minimap/components/_minimap-theme.scss b/packages/stark-ui/src/modules/minimap/components/_minimap-theme.scss
index 48d67614fa..7d992081d6 100644
--- a/packages/stark-ui/src/modules/minimap/components/_minimap-theme.scss
+++ b/packages/stark-ui/src/modules/minimap/components/_minimap-theme.scss
@@ -33,7 +33,7 @@
& .stark-minimap-dropdown-toggle-menu {
border-color: mat-color($grey-palette, 500);
box-shadow: 0 1px 2px mat-color($grey-palette, 300);
- background-color: #FFF;
+ background-color: #fff;
& mat-checkbox {
&:hover {
background: $md-primary-alpha-05;
@@ -85,7 +85,7 @@
& .stark-minimap-dropdown-toggle-menu {
border-color: mat-color($grey-palette, 500);
box-shadow: 0 1px 2px mat-color($grey-palette, 300);
- background-color: #FFF;
+ background-color: #fff;
& mat-checkbox {
&:hover {
background: $md-primary-alpha-05;
diff --git a/packages/stark-ui/src/modules/session-ui.ts b/packages/stark-ui/src/modules/session-ui.ts
index 112375eb84..1ff9f56d13 100644
--- a/packages/stark-ui/src/modules/session-ui.ts
+++ b/packages/stark-ui/src/modules/session-ui.ts
@@ -1,2 +1,5 @@
export * from "./session-ui/session-ui.module";
export * from "./session-ui/pages";
+export * from "./session-ui/effects";
+export * from "./session-ui/entities";
+export * from "./session-ui/components";
diff --git a/packages/stark-ui/src/modules/session-ui/assets/translations/en.ts b/packages/stark-ui/src/modules/session-ui/assets/translations/en.ts
index 56c4569d37..c2259f51f6 100644
--- a/packages/stark-ui/src/modules/session-ui/assets/translations/en.ts
+++ b/packages/stark-ui/src/modules/session-ui/assets/translations/en.ts
@@ -22,6 +22,13 @@ export const translationsEn: object = {
SESSION_LOGOUT: {
TITLE: "Logged out",
LOGIN: "Log in again"
+ },
+ SESSION_TIMEOUT: {
+ SECONDS: " seconds.",
+ STAY_CONNECTED: "Stay connected",
+ TITLE: "Session about to expire",
+ WILL_EXPIRE_IN: "Your session will expire in ",
+ WISH_STAY_CONNECTED: "Do you wish to stay connected?"
}
}
};
diff --git a/packages/stark-ui/src/modules/session-ui/assets/translations/fr.ts b/packages/stark-ui/src/modules/session-ui/assets/translations/fr.ts
index 849abd6ff3..7039bf5cbf 100644
--- a/packages/stark-ui/src/modules/session-ui/assets/translations/fr.ts
+++ b/packages/stark-ui/src/modules/session-ui/assets/translations/fr.ts
@@ -22,6 +22,13 @@ export const translationsFr: object = {
SESSION_LOGOUT: {
TITLE: "Déconnecté",
LOGIN: "Connexion"
+ },
+ SESSION_TIMEOUT: {
+ SECONDS: " secondes.",
+ STAY_CONNECTED: "Rester connecté",
+ TITLE: "Session sur le point d'expirer",
+ WILL_EXPIRE_IN: "Votre session va expirer dans ",
+ WISH_STAY_CONNECTED: "Voulez-vous rester connecté?"
}
}
};
diff --git a/packages/stark-ui/src/modules/session-ui/assets/translations/nl.ts b/packages/stark-ui/src/modules/session-ui/assets/translations/nl.ts
index d0c7ecd62e..8d57c57d4f 100644
--- a/packages/stark-ui/src/modules/session-ui/assets/translations/nl.ts
+++ b/packages/stark-ui/src/modules/session-ui/assets/translations/nl.ts
@@ -22,6 +22,13 @@ export const translationsNl: object = {
SESSION_LOGOUT: {
TITLE: "Afgemeld",
LOGIN: "Opnieuw aanmelden"
+ },
+ SESSION_TIMEOUT: {
+ SECONDS: " seconden vervallen.",
+ STAY_CONNECTED: "Blijf verbonden",
+ TITLE: "Sessie verlopen",
+ WILL_EXPIRE_IN: "Uw sessie zal binnen ",
+ WISH_STAY_CONNECTED: "Wilt u verbonden blijven?"
}
}
};
diff --git a/packages/stark-ui/src/modules/session-ui/components.ts b/packages/stark-ui/src/modules/session-ui/components.ts
new file mode 100644
index 0000000000..ea8b6d947d
--- /dev/null
+++ b/packages/stark-ui/src/modules/session-ui/components.ts
@@ -0,0 +1 @@
+export * from "./components/session-timeout-warning-dialog.component";
diff --git a/packages/stark-ui/src/modules/session-ui/components/_session-timeout-warning-dialog.component.scss b/packages/stark-ui/src/modules/session-ui/components/_session-timeout-warning-dialog.component.scss
new file mode 100644
index 0000000000..fd3227592a
--- /dev/null
+++ b/packages/stark-ui/src/modules/session-ui/components/_session-timeout-warning-dialog.component.scss
@@ -0,0 +1,13 @@
+/* ============================================================================== */
+/* S t a r k S e s s i o n T i m e o u t W a r n i n g D i a l o g */
+/* ============================================================================== */
+/* stark-ui: src/modules/session-ui/components/_session-timeout-warning-dialog.component.scss */
+
+.stark-timeout-warning-dialog {
+ margin: 16px auto;
+ box-sizing: border-box;
+ border-radius: 8px;
+ text-align: center;
+}
+
+/* END stark-ui: src/modules/session-ui/components/_session-timeout-warning-dialog.component.scss */
diff --git a/packages/stark-ui/src/modules/session-ui/components/session-timeout-warning-dialog.component.html b/packages/stark-ui/src/modules/session-ui/components/session-timeout-warning-dialog.component.html
new file mode 100644
index 0000000000..74cc1e5eac
--- /dev/null
+++ b/packages/stark-ui/src/modules/session-ui/components/session-timeout-warning-dialog.component.html
@@ -0,0 +1,20 @@
+
+
STARK.SESSION_TIMEOUT.TITLE
+
+
+ STARK.SESSION_TIMEOUT.WILL_EXPIRE_IN {{ countdown$|async }}
+ STARK.SESSION_TIMEOUT.SECONDS
+
+
STARK.SESSION_TIMEOUT.WISH_STAY_CONNECTED
+
+
+
+
+
+
+
diff --git a/packages/stark-ui/src/modules/session-ui/components/session-timeout-warning-dialog.component.spec.ts b/packages/stark-ui/src/modules/session-ui/components/session-timeout-warning-dialog.component.spec.ts
new file mode 100644
index 0000000000..73d5e03f28
--- /dev/null
+++ b/packages/stark-ui/src/modules/session-ui/components/session-timeout-warning-dialog.component.spec.ts
@@ -0,0 +1,70 @@
+/*tslint:disable:completed-docs*/
+import { async, ComponentFixture, fakeAsync, TestBed, tick } from "@angular/core/testing";
+import { CommonModule } from "@angular/common";
+import { STARK_LOGGING_SERVICE } from "@nationalbankbelgium/stark-core";
+import { MockStarkLoggingService } from "@nationalbankbelgium/stark-core/testing";
+import { MatDialog, MAT_DIALOG_DATA, MatDialogRef, MatDialogModule } from "@angular/material/dialog";
+import { StarkSessionTimeoutWarningDialogComponent } from "./session-timeout-warning-dialog.component";
+
+import Spy = jasmine.Spy;
+import createSpyObj = jasmine.createSpyObj;
+import { Observer } from "rxjs";
+
+describe("SessionTimeoutWarningDialogComponent", () => {
+ let component: StarkSessionTimeoutWarningDialogComponent;
+ let fixture: ComponentFixture;
+
+ let mockDialogRef: MatDialogRef;
+ const mockLogger: MockStarkLoggingService = new MockStarkLoggingService();
+
+ beforeEach(async(() => {
+ return TestBed.configureTestingModule({
+ declarations: [StarkSessionTimeoutWarningDialogComponent],
+ imports: [CommonModule, MatDialogModule],
+ providers: [
+ { provide: STARK_LOGGING_SERVICE, useValue: mockLogger },
+ { provide: MatDialog, useValue: MatDialog },
+ { provide: MAT_DIALOG_DATA, useValue: 20 },
+ { provide: MatDialogRef, useValue: createSpyObj("MatDialogRefSpy", ["close"]) }
+ ]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(StarkSessionTimeoutWarningDialogComponent);
+ mockDialogRef = TestBed.get(MatDialogRef);
+ component = fixture.componentInstance;
+
+ (mockLogger.debug).calls.reset();
+ });
+
+ describe("ngOnInit", () => {
+ it("should set the countdown and decrement it every second", fakeAsync(() => {
+ const mockObserver: Observer = createSpyObj>("observerSpy", ["next", "error", "complete"]);
+
+ component.ngOnInit();
+ component.countdown$.subscribe(mockObserver);
+
+ expect(mockLogger.debug).toHaveBeenCalledTimes(1);
+
+ tick(20000);
+
+ expect(mockObserver.next).toHaveBeenCalledTimes(21);
+ expect(mockObserver.error).not.toHaveBeenCalled();
+ expect(mockObserver.complete).toHaveBeenCalled();
+
+ expect(mockDialogRef.close).toHaveBeenCalledTimes(1);
+ expect(mockDialogRef.close).toHaveBeenCalledWith("countdown-finished");
+ }));
+ });
+
+ describe("keepSession", () => {
+ it("should close the windows when the button is clicked", fakeAsync(() => {
+ component.ngOnInit();
+ component.keepSession();
+
+ expect(mockDialogRef.close).toHaveBeenCalledTimes(1);
+ expect(mockDialogRef.close).toHaveBeenCalledWith("keep-logged");
+ }));
+ });
+});
diff --git a/packages/stark-ui/src/modules/session-ui/components/session-timeout-warning-dialog.component.ts b/packages/stark-ui/src/modules/session-ui/components/session-timeout-warning-dialog.component.ts
new file mode 100644
index 0000000000..2864f6f975
--- /dev/null
+++ b/packages/stark-ui/src/modules/session-ui/components/session-timeout-warning-dialog.component.ts
@@ -0,0 +1,55 @@
+import { Component, Inject, OnInit, ViewEncapsulation } from "@angular/core";
+import { MAT_DIALOG_DATA, MatDialogRef } from "@angular/material/dialog";
+import { Observable, interval } from "rxjs";
+import { STARK_LOGGING_SERVICE, StarkLoggingService } from "@nationalbankbelgium/stark-core";
+import { map, startWith, tap, take } from "rxjs/operators";
+
+/**
+ * The name of the component
+ */
+const componentName: string = "stark-session-timeout-warning-dialog";
+
+/**
+ * Component to display a session timeout warning dialog
+ */
+@Component({
+ selector: "session-timeout-warning-dialog",
+ templateUrl: "./session-timeout-warning-dialog.component.html",
+ encapsulation: ViewEncapsulation.None
+})
+export class StarkSessionTimeoutWarningDialogComponent implements OnInit {
+ /**
+ * countdown Time in seconds before the current session expires.
+ */
+ public countdown$: Observable;
+
+ public constructor(
+ @Inject(STARK_LOGGING_SERVICE) public logger: StarkLoggingService,
+ @Inject(MatDialogRef) private dialogRef: MatDialogRef,
+ @Inject(MAT_DIALOG_DATA) private coutdown: number
+ ) {}
+
+ /**
+ * Component lifecycle hook
+ */
+ public ngOnInit(): void {
+ this.logger.debug(componentName + ": controller initialized");
+ this.countdown$ = interval(1000).pipe(
+ take(this.coutdown),
+ startWith(-1),
+ map((value: number) => this.coutdown - value - 1), // -1 due to the delay of the dialog animation
+ tap((value: number) => {
+ if (value === 0) {
+ this.dialogRef.close("countdown-finished");
+ }
+ })
+ );
+ }
+
+ /**
+ * This methods is used to close the dialog and to send an answer indicating that the user should keep logged.
+ */
+ public keepSession(): void {
+ this.dialogRef.close("keep-logged");
+ }
+}
diff --git a/packages/stark-ui/src/modules/session-ui/effects.ts b/packages/stark-ui/src/modules/session-ui/effects.ts
new file mode 100644
index 0000000000..1caddc8346
--- /dev/null
+++ b/packages/stark-ui/src/modules/session-ui/effects.ts
@@ -0,0 +1 @@
+export * from "./effects/session-timeout-warning.effects";
diff --git a/packages/stark-ui/src/modules/session-ui/effects/session-timeout-warning.effect.spec.ts b/packages/stark-ui/src/modules/session-ui/effects/session-timeout-warning.effect.spec.ts
new file mode 100644
index 0000000000..4f4cc6cd4c
--- /dev/null
+++ b/packages/stark-ui/src/modules/session-ui/effects/session-timeout-warning.effect.spec.ts
@@ -0,0 +1,224 @@
+/*tslint:disable:completed-docs no-big-function*/
+import createSpyObj = jasmine.createSpyObj;
+import { Observable, ReplaySubject, Observer, Subject } from "rxjs";
+import { async, TestBed } from "@angular/core/testing";
+import {
+ STARK_SESSION_SERVICE,
+ StarkSessionService,
+ StarkSessionTimeoutCountdownStart,
+ StarkSessionTimeoutCountdownFinish,
+ StarkInitializeSession
+} from "@nationalbankbelgium/stark-core";
+import { MockStarkSessionService } from "@nationalbankbelgium/stark-core/testing";
+import { provideMockActions } from "@ngrx/effects/testing";
+import { EffectNotification } from "@ngrx/effects";
+import { TranslateModule, TranslateService } from "@ngx-translate/core";
+import { MatDialog, MatDialogModule } from "@angular/material/dialog";
+import { MatButtonModule } from "@angular/material/button";
+import { NoopAnimationsModule } from "@angular/platform-browser/animations";
+import { StarkSessionTimeoutWarningDialogComponent } from "../components/session-timeout-warning-dialog.component";
+import { StarkSessionTimeoutWarningDialogEffects } from "../effects/session-timeout-warning.effects";
+import { STARK_SESSION_UI_CONFIG, StarkSessionUiConfig } from "../entities/stark-session-ui-config";
+
+import Spy = jasmine.Spy;
+
+describe("Effects: StarkSessionTimeoutWarningDialogEffects", () => {
+ let effectsClass: StarkSessionTimeoutWarningDialogEffects;
+ let mockSessionService: StarkSessionService;
+ let mockDialogService: MatDialog;
+ let mockSessionUiConfig: StarkSessionUiConfig;
+ let actions: Observable;
+
+ beforeEach(async(() => {
+ return TestBed.configureTestingModule({
+ imports: [NoopAnimationsModule, MatDialogModule, TranslateModule.forRoot(), MatButtonModule],
+ providers: [
+ StarkSessionTimeoutWarningDialogEffects,
+ provideMockActions(() => actions),
+ TranslateService,
+ {
+ provide: MatDialog,
+ useValue: createSpyObj("MatDialogSpy", ["open", "close", "closeAll"])
+ },
+ { provide: StarkSessionTimeoutWarningDialogComponent, useValue: StarkSessionTimeoutWarningDialogComponent },
+ { provide: STARK_SESSION_SERVICE, useFactory: () => new MockStarkSessionService() },
+ { provide: STARK_SESSION_UI_CONFIG, useValue: new StarkSessionUiConfig() }
+ ]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ effectsClass = TestBed.get(StarkSessionTimeoutWarningDialogEffects);
+ mockSessionService = TestBed.get(STARK_SESSION_SERVICE);
+ mockDialogService = TestBed.get(MatDialog);
+ mockSessionUiConfig = TestBed.get(STARK_SESSION_UI_CONFIG);
+ });
+
+ describe("on initialization", () => {
+ it("should set internal component properties", () => {
+ expect(effectsClass.sessionService).not.toBeNull();
+ expect(effectsClass.sessionService).toBeDefined();
+ expect(effectsClass.starkSessionUiConfig).not.toBeNull();
+ expect(effectsClass.starkSessionUiConfig).toBeDefined();
+ });
+ });
+
+ describe("On StarkSessionTimeoutWarning$", () => {
+ it("Should open a dialog when the timeout countdown begins", () => {
+ const afterClosedResult: string = "keep-logged";
+ const afterClosed$: Subject = new Subject();
+
+ (mockDialogService.open).and.returnValue({
+ afterClosed: () => {
+ return afterClosed$;
+ }
+ });
+
+ const mockObserver: Observer = createSpyObj>("observerSpy", ["next", "error", "complete"]);
+ const subject: ReplaySubject = new ReplaySubject(1);
+ actions = subject.asObservable();
+
+ effectsClass.starkSessionTimeoutWarning$().subscribe(mockObserver);
+
+ expect(mockSessionService.pauseUserActivityTracking).not.toHaveBeenCalled();
+ expect(mockDialogService.open).not.toHaveBeenCalled();
+
+ subject.next(new StarkSessionTimeoutCountdownStart(20));
+
+ expect(mockSessionService.pauseUserActivityTracking).toHaveBeenCalled();
+ expect(mockObserver.next).toHaveBeenCalledTimes(1);
+ expect(mockObserver.error).not.toHaveBeenCalled();
+ expect(mockObserver.complete).not.toHaveBeenCalled();
+ expect(mockDialogService.open).toHaveBeenCalledTimes(1);
+ expect(mockDialogService.open).toHaveBeenCalledWith(StarkSessionTimeoutWarningDialogComponent, { data: 20 });
+
+ expect(mockSessionService.resumeUserActivityTracking).not.toHaveBeenCalled();
+
+ afterClosed$.next(afterClosedResult);
+
+ expect(mockSessionService.resumeUserActivityTracking).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe("On StarkSessionTimeoutWarningClose$", () => {
+ it("Should close the dialog when the countdown finishes", () => {
+ const mockObserver: Observer = createSpyObj>("observerSpy", ["next", "error", "complete"]);
+
+ const subject: ReplaySubject = new ReplaySubject(1);
+ actions = subject.asObservable();
+
+ effectsClass.starkSessionTimeoutWarningClose$().subscribe(mockObserver);
+
+ expect(mockDialogService.closeAll).not.toHaveBeenCalled();
+
+ subject.next(new StarkSessionTimeoutCountdownFinish());
+
+ expect(mockObserver.next).toHaveBeenCalledTimes(1);
+ expect(mockObserver.error).not.toHaveBeenCalled();
+ expect(mockObserver.complete).not.toHaveBeenCalled();
+ expect(mockDialogService.closeAll).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe("On ngrxOnRunEffects", () => {
+ it("Should stop the effects immediately when the option timeoutWarningDialogDisabled is true", () => {
+ const mockObserver: Observer = createSpyObj>("observerSpy", ["next", "error", "complete"]);
+
+ const actions$: ReplaySubject = new ReplaySubject(1);
+ actions = actions$.asObservable();
+ spyOn(effectsClass.actions$, "pipe").and.callThrough();
+
+ mockSessionUiConfig.timeoutWarningDialogDisabled = true;
+
+ const mockResolvedEffectsSubject: Subject = new Subject();
+ const mockResolvedEffects$: Observable = mockResolvedEffectsSubject.asObservable();
+
+ const resolvedEffectsObservable: Observable = effectsClass.ngrxOnRunEffects(mockResolvedEffects$);
+ expect(effectsClass.actions$.pipe).toHaveBeenCalledTimes(1);
+
+ resolvedEffectsObservable.subscribe(mockObserver);
+
+ (mockObserver.next).calls.reset();
+
+ actions$.next("dummy acton");
+ actions$.next("another dummy acton");
+
+ actions$.next(new StarkInitializeSession({}));
+ mockResolvedEffectsSubject.next("this should never be emitted");
+
+ expect(mockObserver.next).not.toHaveBeenCalled();
+ expect(mockObserver.error).not.toHaveBeenCalled();
+ expect(mockObserver.complete).not.toHaveBeenCalled();
+ });
+
+ it("Should run the effects immediately when the option timeoutWarningDialogDisabled is false", () => {
+ const mockObserver: Observer = createSpyObj>("observerSpy", ["next", "error", "complete"]);
+
+ const actions$: ReplaySubject = new ReplaySubject(1);
+ actions = actions$.asObservable();
+ spyOn(effectsClass.actions$, "pipe").and.callThrough();
+
+ mockSessionUiConfig.timeoutWarningDialogDisabled = false;
+
+ const mockResolvedEffectsSubject: Subject = new Subject();
+ const mockResolvedEffects$: Observable = mockResolvedEffectsSubject.asObservable();
+
+ const resolvedEffectsObservable: Observable = effectsClass.ngrxOnRunEffects(mockResolvedEffects$);
+ expect(effectsClass.actions$.pipe).toHaveBeenCalledTimes(1);
+
+ resolvedEffectsObservable.subscribe(mockObserver);
+
+ actions$.next("dummy initial action1");
+ mockResolvedEffectsSubject.next("dummy resolved effect");
+
+ expect(mockObserver.next).toHaveBeenCalled();
+ expect(mockObserver.error).not.toHaveBeenCalled();
+ expect(mockObserver.complete).not.toHaveBeenCalled();
+
+ (mockObserver.next).calls.reset();
+
+ // the effects should keep on running with any action
+ actions$.next("dummy action1");
+ mockResolvedEffectsSubject.next("another dummy resolved effect");
+
+ expect(mockObserver.next).toHaveBeenCalled();
+ expect(mockObserver.error).not.toHaveBeenCalled();
+ expect(mockObserver.complete).not.toHaveBeenCalled();
+ });
+
+ it("Should run the effects immediately when the option timeoutWarningDialogDisabled is undefined", () => {
+ const mockObserver: Observer = createSpyObj>("observerSpy", ["next", "error", "complete"]);
+
+ const actions$: ReplaySubject = new ReplaySubject(1);
+ actions = actions$.asObservable();
+ spyOn(effectsClass.actions$, "pipe").and.callThrough();
+
+ mockSessionUiConfig.timeoutWarningDialogDisabled = undefined;
+
+ const mockResolvedEffectsSubject: Subject = new Subject();
+ const mockResolvedEffects$: Observable = mockResolvedEffectsSubject.asObservable();
+
+ const resolvedEffectsObservable: Observable = effectsClass.ngrxOnRunEffects(mockResolvedEffects$);
+ expect(effectsClass.actions$.pipe).toHaveBeenCalledTimes(1);
+
+ resolvedEffectsObservable.subscribe(mockObserver);
+
+ actions$.next("dummy initial action1");
+ mockResolvedEffectsSubject.next("dummy resolved effect");
+
+ expect(mockObserver.next).toHaveBeenCalled();
+ expect(mockObserver.error).not.toHaveBeenCalled();
+ expect(mockObserver.complete).not.toHaveBeenCalled();
+
+ (mockObserver.next).calls.reset();
+
+ // the effects should keep on running with any action
+ actions$.next("dummy action1");
+ mockResolvedEffectsSubject.next("another dummy resolved effect");
+
+ expect(mockObserver.next).toHaveBeenCalled();
+ expect(mockObserver.error).not.toHaveBeenCalled();
+ expect(mockObserver.complete).not.toHaveBeenCalled();
+ });
+ });
+});
diff --git a/packages/stark-ui/src/modules/session-ui/effects/session-timeout-warning.effects.ts b/packages/stark-ui/src/modules/session-ui/effects/session-timeout-warning.effects.ts
new file mode 100644
index 0000000000..e72cb00521
--- /dev/null
+++ b/packages/stark-ui/src/modules/session-ui/effects/session-timeout-warning.effects.ts
@@ -0,0 +1,84 @@
+import { Inject, Injectable, Optional } from "@angular/core";
+import { Observable, of } from "rxjs";
+import { exhaustMap, map, takeUntil } from "rxjs/operators";
+import {
+ STARK_SESSION_SERVICE,
+ StarkSessionActionTypes,
+ StarkSessionService,
+ StarkSessionTimeoutCountdownStart,
+ StarkSessionTimeoutCountdownFinish
+} from "@nationalbankbelgium/stark-core";
+import { StarkSessionTimeoutWarningDialogComponent } from "../components/session-timeout-warning-dialog.component";
+import { MatDialog } from "@angular/material/dialog";
+import { Actions, Effect, EffectNotification, ofType, OnRunEffects } from "@ngrx/effects";
+import { STARK_SESSION_UI_CONFIG, StarkSessionUiConfig } from "../entities/stark-session-ui-config";
+
+@Injectable()
+export class StarkSessionTimeoutWarningDialogEffects implements OnRunEffects {
+ /**
+ * Show a warning dialog when the Timeout countdown stars (the user activity tracking is paused until the user closes the dialog)
+ */
+ public constructor(
+ public actions$: Actions,
+ @Inject(STARK_SESSION_SERVICE) public sessionService: StarkSessionService,
+ @Optional()
+ @Inject(STARK_SESSION_UI_CONFIG)
+ public starkSessionUiConfig: StarkSessionUiConfig,
+ @Inject(MatDialog) public dialogService: MatDialog
+ ) {}
+
+ /**
+ * This method is used to display a warning session timeout dialog.
+ * When the dialog is closed, if the "keep-logged" string is received as a result, it means that the user should keep logged
+ * and we resume the user activity tracking.
+ */
+ @Effect({ dispatch: false })
+ public starkSessionTimeoutWarning$(): Observable {
+ return this.actions$.pipe(
+ ofType(StarkSessionActionTypes.SESSION_TIMEOUT_COUNTDOWN_START),
+ map((action: StarkSessionTimeoutCountdownStart) => {
+ this.sessionService.pauseUserActivityTracking();
+ this.dialogService
+ .open(StarkSessionTimeoutWarningDialogComponent, { data: action.countdown })
+ .afterClosed()
+ .subscribe((result: string) => {
+ if (result && result === "keep-logged") {
+ this.sessionService.resumeUserActivityTracking();
+ }
+ });
+ })
+ );
+ }
+
+ /**
+ * This method is used to close the dialog if the session timeout countdown has finished
+ */
+ @Effect({ dispatch: false })
+ public starkSessionTimeoutWarningClose$(): Observable {
+ return this.actions$.pipe(
+ ofType(StarkSessionActionTypes.SESSION_TIMEOUT_COUNTDOWN_FINISH),
+ map(() => {
+ this.dialogService.closeAll();
+ })
+ );
+ }
+
+ /**
+ * This method will be triggered before the session is open to determine if we should use the session timeout warning effect or not.
+ */
+ public ngrxOnRunEffects(resolvedEffects$: Observable): Observable {
+ if (this.starkSessionUiConfig && this.starkSessionUiConfig.timeoutWarningDialogDisabled === true) {
+ return this.actions$.pipe(
+ exhaustMap(() => {
+ return resolvedEffects$.pipe(takeUntil(of("stop")));
+ })
+ );
+ } else {
+ return this.actions$.pipe(
+ exhaustMap(() => {
+ return resolvedEffects$;
+ })
+ );
+ }
+ }
+}
diff --git a/packages/stark-ui/src/modules/session-ui/entities.ts b/packages/stark-ui/src/modules/session-ui/entities.ts
new file mode 100644
index 0000000000..4bd41d1679
--- /dev/null
+++ b/packages/stark-ui/src/modules/session-ui/entities.ts
@@ -0,0 +1 @@
+export * from "./entities/stark-session-ui-config";
diff --git a/packages/stark-ui/src/modules/session-ui/entities/stark-session-ui-config.ts b/packages/stark-ui/src/modules/session-ui/entities/stark-session-ui-config.ts
new file mode 100644
index 0000000000..95dc5f952f
--- /dev/null
+++ b/packages/stark-ui/src/modules/session-ui/entities/stark-session-ui-config.ts
@@ -0,0 +1,19 @@
+import { InjectionToken } from "@angular/core";
+
+/**
+ * The InjectionToken version of the config name
+ */
+export const STARK_SESSION_UI_CONFIG: InjectionToken = new InjectionToken(
+ "StarkSessionUiConfig"
+);
+
+/**
+ * Definition of the configuration object for the Stark Session UI module
+ */
+export class StarkSessionUiConfig {
+ /**
+ * Whether the warning dialog should be displayed or not.
+ * If true, then the Ngrx Effects in charge of displaying the dialog won't be executed.
+ */
+ public timeoutWarningDialogDisabled?: boolean;
+}
diff --git a/packages/stark-ui/src/modules/session-ui/pages/preloading/preloading-page.component.ts b/packages/stark-ui/src/modules/session-ui/pages/preloading/preloading-page.component.ts
index 0f418afd74..89d7f63d6f 100644
--- a/packages/stark-ui/src/modules/session-ui/pages/preloading/preloading-page.component.ts
+++ b/packages/stark-ui/src/modules/session-ui/pages/preloading/preloading-page.component.ts
@@ -21,7 +21,7 @@ const componentName: string = "stark-preloading-page";
/**
* Preloading Page smart component.
- *
+ *
* This page will be shown when the application starts and will fetch the user profile (via the {@link StarkUserService}) to perform the login of the user.
* It will redirect to the target page (via the {@link StarkRoutingService}) as soon as the user profile is loaded and logged in.
*/
diff --git a/packages/stark-ui/src/modules/session-ui/pages/session-expired/session-expired-page.component.ts b/packages/stark-ui/src/modules/session-ui/pages/session-expired/session-expired-page.component.ts
index b6295d4b75..66229091f4 100644
--- a/packages/stark-ui/src/modules/session-ui/pages/session-expired/session-expired-page.component.ts
+++ b/packages/stark-ui/src/modules/session-ui/pages/session-expired/session-expired-page.component.ts
@@ -9,7 +9,7 @@ const componentName: string = "stark-session-expired-page";
/**
* Session Expired Page smart component.
- *
+ *
* This page will be shown when there is no user activity in the application and the session expiration timer has timed out (see {@link StarKApplicationConfig}).
* In this page, the user has the ability to reload again the application clicking the Reload button.
*/
diff --git a/packages/stark-ui/src/modules/session-ui/pages/session-logout/session-logout-page.component.ts b/packages/stark-ui/src/modules/session-ui/pages/session-logout/session-logout-page.component.ts
index 133ef21e98..394ff85dd6 100644
--- a/packages/stark-ui/src/modules/session-ui/pages/session-logout/session-logout-page.component.ts
+++ b/packages/stark-ui/src/modules/session-ui/pages/session-logout/session-logout-page.component.ts
@@ -9,8 +9,8 @@ const componentName: string = "stark-session-logout-page";
/**
* Session Logout Page smart component.
- *
- * This page will be shown when the user logs out from the application (i.e. clicking the {@AppLogoComponent} button).
+ *
+ * This page will be shown when the user logs out from the application (i.e. clicking the {@AppLogoComponent} button).
* In this page, the user has the ability to reload and log in again into the application by clicking the Login button.
*/
@Component({
diff --git a/packages/stark-ui/src/modules/session-ui/session-ui.module.ts b/packages/stark-ui/src/modules/session-ui/session-ui.module.ts
index dc2d3c0a34..ce8afa522e 100644
--- a/packages/stark-ui/src/modules/session-ui/session-ui.module.ts
+++ b/packages/stark-ui/src/modules/session-ui/session-ui.module.ts
@@ -3,7 +3,6 @@ import { UIRouterModule } from "@uirouter/angular";
import { TranslateModule, TranslateService } from "@ngx-translate/core";
import { CommonModule } from "@angular/common";
import { MatButtonModule } from "@angular/material/button";
-import { StarkLocale } from "@nationalbankbelgium/stark-core";
import { SESSION_UI_STATES } from "./routes";
import {
StarkLoginPageComponent,
@@ -16,22 +15,39 @@ import { translationsFr } from "./assets/translations/fr";
import { translationsNl } from "./assets/translations/nl";
import { mergeUiTranslations } from "../../common/translations";
+import { STARK_SESSION_UI_CONFIG, StarkSessionUiConfig } from "./entities";
+import { StarkSessionTimeoutWarningDialogComponent } from "./components/session-timeout-warning-dialog.component";
+import { MatDialogModule } from "@angular/material/dialog";
+import { StarkLocale } from "@nationalbankbelgium/stark-core";
+import { EffectsModule } from "@ngrx/effects";
+import { StarkSessionTimeoutWarningDialogEffects } from "./effects";
+
@NgModule({
declarations: [
StarkLoginPageComponent,
StarkPreloadingPageComponent,
StarkSessionExpiredPageComponent,
- StarkSessionLogoutPageComponent
+ StarkSessionLogoutPageComponent,
+ StarkSessionTimeoutWarningDialogComponent
+ ],
+ exports: [
+ StarkLoginPageComponent,
+ StarkPreloadingPageComponent,
+ StarkSessionExpiredPageComponent,
+ StarkSessionLogoutPageComponent,
+ StarkSessionTimeoutWarningDialogComponent
],
- exports: [StarkLoginPageComponent, StarkPreloadingPageComponent, StarkSessionExpiredPageComponent, StarkSessionLogoutPageComponent],
imports: [
CommonModule,
UIRouterModule.forChild({
states: SESSION_UI_STATES
}),
MatButtonModule,
- TranslateModule
- ]
+ MatDialogModule,
+ TranslateModule,
+ EffectsModule.forFeature([StarkSessionTimeoutWarningDialogEffects])
+ ],
+ entryComponents: [StarkSessionTimeoutWarningDialogComponent]
})
export class StarkSessionUiModule {
/**
@@ -40,9 +56,10 @@ export class StarkSessionUiModule {
* @link https://angular.io/guide/singleton-services#forroot
* @returns a module with providers
*/
- public static forRoot(): ModuleWithProviders {
+ public static forRoot(starkSessionUiConfig?: StarkSessionUiConfig): ModuleWithProviders {
return {
- ngModule: StarkSessionUiModule
+ ngModule: StarkSessionUiModule,
+ providers: [starkSessionUiConfig ? { provide: STARK_SESSION_UI_CONFIG, useValue: starkSessionUiConfig } : []]
};
}
diff --git a/showcase/src/app/getting-started/getting-started-component/_getting-started-theme.scss b/showcase/src/app/getting-started/getting-started-component/_getting-started-theme.scss
index f9b9bd7089..1c2e4bf0f1 100644
--- a/showcase/src/app/getting-started/getting-started-component/_getting-started-theme.scss
+++ b/showcase/src/app/getting-started/getting-started-component/_getting-started-theme.scss
@@ -24,7 +24,7 @@
.custom-pretty-print {
& pre {
background-color: $custom-dark-background;
- border-radius: 0.3em;
+ border-radius: 0.3em;
}
color: mat-color($grey-palette, 100);
}
diff --git a/showcase/src/app/shared/table-of-contents/table-of-contents.component.scss b/showcase/src/app/shared/table-of-contents/table-of-contents.component.scss
index bd161086a3..219400b959 100644
--- a/showcase/src/app/shared/table-of-contents/table-of-contents.component.scss
+++ b/showcase/src/app/shared/table-of-contents/table-of-contents.component.scss
@@ -27,33 +27,33 @@
cursor: pointer;
}
-@media #{$desktop-lg-query} {
+@media #{$desktop-lg-query} {
.table-of-contents-container {
- padding: 5px 0 10px 10px;
- width: 200px;
- position: fixed;
- top: 145px;
- transform: translateX(375%);
+ padding: 5px 0 10px 10px;
+ width: 200px;
+ position: fixed;
+ top: 145px;
+ transform: translateX(375%);
}
}
-@media #{$handset-toc-query-screen}{
+@media #{$handset-toc-query-screen} {
.table-of-contents-container {
- padding: 5px 0 10px 10px;
+ padding: 5px 0 10px 10px;
width: 500px;
left: 40px;
position: inherit;
- display: inline-table;;
+ display: inline-table;
}
}
@media #{$tablet-toc-query-screen} {
.table-of-contents-container {
- padding: 5px 0 10px 10px;
- position: fixed;
- top: 145px;
- width: 200px;
- display: inline-table;
- right: 0;
+ padding: 5px 0 10px 10px;
+ position: fixed;
+ top: 145px;
+ width: 200px;
+ display: inline-table;
+ right: 0;
}
}
diff --git a/starter/src/styles/_stark-styles.scss b/starter/src/styles/_stark-styles.scss
index 4bab25c686..1c2e0fd391 100644
--- a/starter/src/styles/_stark-styles.scss
+++ b/starter/src/styles/_stark-styles.scss
@@ -17,6 +17,7 @@ IMPORTANT: Stark styles are provided as SCSS styles so they should be imported i
@import "~@nationalbankbelgium/stark-ui/src/modules/language-selector/components/language-selector.component";
@import "~@nationalbankbelgium/stark-ui/src/modules/toast-notification/components/toast-notification.component";
@import "~@nationalbankbelgium/stark-ui/src/modules/toast-notification/components/toast-notification-theme";
+@import "~@nationalbankbelgium/stark-ui/src/modules/session-ui/components/session-timeout-warning-dialog.component";
/* Stark session-ui pages */
@import "~@nationalbankbelgium/stark-ui/src/modules/session-ui/pages/login/login-page.component";