keycloak-uncached

KEYCLOAK-7857: Fix notifications

7/23/2018 5:04:59 PM

Changes

services/src/main/java/org/keycloak/services/resources/account/Errors.java 30(+0 -30)

themes/src/main/resources/theme/keycloak-preview/account/resources/app/page/side-nav-item.ts 33(+0 -33)

themes/src/main/resources/theme/keycloak-preview/account/resources/app/side-nav/side-nav.component.css 0(+0 -0)

themes/src/main/resources/theme/keycloak-preview/account/resources/app/side-nav/side-nav.component.html 71(+0 -71)

themes/src/main/resources/theme/keycloak-preview/account/resources/app/side-nav/side-nav.component.ts 115(+0 -115)

themes/src/main/resources/theme/keycloak-preview/account/resources/app/top-nav/notification.component.css 0(+0 -0)

themes/src/main/resources/theme/keycloak-preview/account/resources/app/top-nav/notification.component.html 12(+0 -12)

themes/src/main/resources/theme/keycloak-preview/account/resources/app/top-nav/notification.component.ts 43(+0 -43)

themes/src/main/resources/theme/keycloak-preview/account/resources/app/top-nav/toast.notifier.ts 75(+0 -75)

themes/src/main/resources/theme/keycloak-preview/account/resources/app/top-nav/top-nav.component.css 0(+0 -0)

themes/src/main/resources/theme/keycloak-preview/account/resources/app/top-nav/top-nav.component.html 42(+0 -42)

themes/src/main/resources/theme/keycloak-preview/account/resources/app/top-nav/top-nav.component.ts 69(+0 -69)

Details

diff --git a/core/src/main/java/org/keycloak/representations/idm/ErrorRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/ErrorRepresentation.java
index f9c9071..64f3484 100644
--- a/core/src/main/java/org/keycloak/representations/idm/ErrorRepresentation.java
+++ b/core/src/main/java/org/keycloak/representations/idm/ErrorRepresentation.java
@@ -22,6 +22,7 @@ package org.keycloak.representations.idm;
  */
 public class ErrorRepresentation {
     private String errorMessage;
+    private Object[] params;
 
     public ErrorRepresentation() {
     }
@@ -33,4 +34,12 @@ public class ErrorRepresentation {
     public void setErrorMessage(String errorMessage) {
         this.errorMessage = errorMessage;
     }
+    
+    public Object[] getParams() {
+        return this.params;
+    }
+    
+    public void setParams(Object[] params) {
+        this.params = params;
+    }
 }
diff --git a/services/src/main/java/org/keycloak/services/ErrorResponse.java b/services/src/main/java/org/keycloak/services/ErrorResponse.java
index cb1ad54..492541f 100755
--- a/services/src/main/java/org/keycloak/services/ErrorResponse.java
+++ b/services/src/main/java/org/keycloak/services/ErrorResponse.java
@@ -32,8 +32,13 @@ public class ErrorResponse {
     }
 
     public static Response error(String message, Response.Status status) {
+        return ErrorResponse.error(message, null, status);
+    }
+    
+    public static Response error(String message, Object[] params, Response.Status status) {
         ErrorRepresentation error = new ErrorRepresentation();
         error.setErrorMessage(message);
+        error.setParams(params);
         return Response.status(status).entity(error).type(MediaType.APPLICATION_JSON).build();
     }
 
diff --git a/services/src/main/java/org/keycloak/services/resources/account/AccountCredentialResource.java b/services/src/main/java/org/keycloak/services/resources/account/AccountCredentialResource.java
index 3af53c5..125de8b 100644
--- a/services/src/main/java/org/keycloak/services/resources/account/AccountCredentialResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/account/AccountCredentialResource.java
@@ -20,6 +20,8 @@ import javax.ws.rs.POST;
 import javax.ws.rs.Path;
 import javax.ws.rs.Produces;
 import javax.ws.rs.core.Response;
+import org.keycloak.models.ModelException;
+import org.keycloak.services.messages.Messages;
 
 public class AccountCredentialResource {
 
@@ -62,10 +64,14 @@ public class AccountCredentialResource {
         UserCredentialModel cred = UserCredentialModel.password(update.getCurrentPassword());
         if (!session.userCredentialManager().isValid(realm, user, cred)) {
             event.error(org.keycloak.events.Errors.INVALID_USER_CREDENTIALS);
-            return ErrorResponse.error(Errors.INVALID_CREDENTIALS, Response.Status.BAD_REQUEST);
+            return ErrorResponse.error(Messages.INVALID_PASSWORD_EXISTING, Response.Status.BAD_REQUEST);
         }
 
-        session.userCredentialManager().updateCredential(realm, user, UserCredentialModel.password(update.getNewPassword(), false));
+        try {
+            session.userCredentialManager().updateCredential(realm, user, UserCredentialModel.password(update.getNewPassword(), false));
+        } catch (ModelException e) {
+            return ErrorResponse.error(e.getMessage(), e.getParameters(), Response.Status.BAD_REQUEST);
+        }
 
         return Response.ok().build();
     }
diff --git a/services/src/main/java/org/keycloak/services/resources/account/AccountRestService.java b/services/src/main/java/org/keycloak/services/resources/account/AccountRestService.java
index 32d88de..48e55ee 100755
--- a/services/src/main/java/org/keycloak/services/resources/account/AccountRestService.java
+++ b/services/src/main/java/org/keycloak/services/resources/account/AccountRestService.java
@@ -54,6 +54,7 @@ import javax.ws.rs.core.UriInfo;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
+import org.keycloak.services.messages.Messages;
 
 /**
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -146,27 +147,27 @@ public class AccountRestService {
                 if (usernameChanged) {
                     UserModel existing = session.users().getUserByUsername(userRep.getUsername(), realm);
                     if (existing != null) {
-                        return ErrorResponse.exists(Errors.USERNAME_EXISTS);
+                        return ErrorResponse.exists(Messages.USERNAME_EXISTS);
                     }
 
                     user.setUsername(userRep.getUsername());
                 }
             } else if (usernameChanged) {
-                return ErrorResponse.error(Errors.READ_ONLY_USERNAME, Response.Status.BAD_REQUEST);
+                return ErrorResponse.error(Messages.READ_ONLY_USERNAME, Response.Status.BAD_REQUEST);
             }
 
             boolean emailChanged = userRep.getEmail() != null && !userRep.getEmail().equals(user.getEmail());
             if (emailChanged && !realm.isDuplicateEmailsAllowed()) {
                 UserModel existing = session.users().getUserByEmail(userRep.getEmail(), realm);
                 if (existing != null) {
-                    return ErrorResponse.exists(Errors.EMAIL_EXISTS);
+                    return ErrorResponse.exists(Messages.EMAIL_EXISTS);
                 }
             }
 
             if (realm.isRegistrationEmailAsUsername() && !realm.isDuplicateEmailsAllowed()) {
                 UserModel existing = session.users().getUserByUsername(userRep.getEmail(), realm);
                 if (existing != null) {
-                    return ErrorResponse.exists(Errors.USERNAME_EXISTS);
+                    return ErrorResponse.exists(Messages.USERNAME_EXISTS);
                 }
             }
 
@@ -200,7 +201,7 @@ public class AccountRestService {
 
             return Cors.add(request, Response.ok()).auth().allowedOrigins(auth.getToken()).build();
         } catch (ReadOnlyException e) {
-            return ErrorResponse.error(Errors.READ_ONLY_USER, Response.Status.BAD_REQUEST);
+            return ErrorResponse.error(Messages.READ_ONLY_USER, Response.Status.BAD_REQUEST);
         }
     }
 
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountRestServiceTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountRestServiceTest.java
index 6dbade1..52ffe3e 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountRestServiceTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountRestServiceTest.java
@@ -41,6 +41,7 @@ import java.util.List;
 
 import static org.hamcrest.Matchers.containsInAnyOrder;
 import static org.junit.Assert.*;
+import org.keycloak.services.messages.Messages;
 
 /**
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -119,7 +120,7 @@ public class AccountRestServiceTest extends AbstractTestRealmKeycloakTest {
         assertEquals("bobby@localhost", user.getEmail());
 
         user.setEmail("john-doh@localhost");
-        updateError(user, 409, "email_exists");
+        updateError(user, 409, Messages.EMAIL_EXISTS);
 
         user.setEmail("test-user@localhost");
         user = updateAndGet(user);
@@ -131,7 +132,7 @@ public class AccountRestServiceTest extends AbstractTestRealmKeycloakTest {
         assertEquals("updatedusername", user.getUsername());
 
         user.setUsername("john-doh@localhost");
-        updateError(user, 409, "username_exists");
+        updateError(user, 409, Messages.USERNAME_EXISTS);
 
         user.setUsername("test-user@localhost");
         user = updateAndGet(user);
@@ -142,7 +143,7 @@ public class AccountRestServiceTest extends AbstractTestRealmKeycloakTest {
         adminClient.realm("test").update(realmRep);
 
         user.setUsername("updatedUsername2");
-        updateError(user, 400, "username_read_only");
+        updateError(user, 400, Messages.READ_ONLY_USERNAME);
     }
 
     private UserRepresentation updateAndGet(UserRepresentation user) throws IOException {
diff --git a/themes/src/main/resources/theme/keycloak-preview/account/resources/app/account-service/account.service.ts b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/account-service/account.service.ts
index 860408d..f13d921 100644
--- a/themes/src/main/resources/theme/keycloak-preview/account/resources/app/account-service/account.service.ts
+++ b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/account-service/account.service.ts
@@ -18,8 +18,10 @@
 import {Injectable} from '@angular/core';
 import {Http, Response, RequestOptionsArgs} from '@angular/http';
 
-import {ToastNotifier, ToastNotification} from '../top-nav/toast.notifier';
+import {KeycloakNotificationService} from '../notification/keycloak-notification.service';
 import {KeycloakService} from '../keycloak-service/keycloak.service';
+
+import {NotificationType} from 'patternfly-ng/notification';
  
  /**
  *
@@ -31,8 +33,8 @@ export class AccountServiceClient {
     private accountUrl: string;
 
     constructor(protected http: Http,
-        protected kcSvc: KeycloakService,
-        protected notifier: ToastNotifier) {
+                protected kcSvc: KeycloakService,
+                protected kcNotifySvc: KeycloakNotificationService) {
         this.accountUrl = kcSvc.authServerUrl() + 'realms/' + kcSvc.realm() + '/account';
     }
     
@@ -56,7 +58,7 @@ export class AccountServiceClient {
     private handleAccountUpdated(responseHandler: Function, res: Response, successMessage?: string) {
         let message: string = "Your account has been updated.";
         if (successMessage) message = successMessage;
-        this.notifier.emit(new ToastNotification(message, "success"));
+        this.kcNotifySvc.notify(message, NotificationType.SUCCESS);
         responseHandler(res);
     } 
     
@@ -102,8 +104,8 @@ export class AccountServiceClient {
         if (not500Error && response.json().hasOwnProperty('error_description')) {
             message = response.json().error_description;
         }
-
-        this.notifier.emit(new ToastNotification(message, "error"));
+        
+        this.kcNotifySvc.notify(message, NotificationType.DANGER, response.json().params);
     }
 }
 
diff --git a/themes/src/main/resources/theme/keycloak-preview/account/resources/app/app.module.ts b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/app.module.ts
index 82a2eec..ce17228 100644
--- a/themes/src/main/resources/theme/keycloak-preview/account/resources/app/app.module.ts
+++ b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/app.module.ts
@@ -28,30 +28,28 @@ import { KeycloakService } from './keycloak-service/keycloak.service';
 import { KEYCLOAK_HTTP_PROVIDER } from './keycloak-service/keycloak.http';
 import {KeycloakGuard} from './keycloak-service/keycloak.guard';
 
-import {ResponsivenessService} from './responsiveness-service/responsiveness.service'
+import {ResponsivenessService} from './responsiveness-service/responsiveness.service';
+import {KeycloakNotificationService} from './notification/keycloak-notification.service';
 
 import { AccountServiceClient } from './account-service/account.service';
 import {TranslateUtil} from './ngx-translate/translate.util';
 
 import { DeclaredVarTranslateLoader } from './ngx-translate/declared.var.translate.loader';
 import { AppComponent } from './app.component';
-import { TopNavComponent } from './top-nav/top-nav.component';
-import { NotificationComponent } from './top-nav/notification.component';
-import { ToastNotifier } from './top-nav/toast.notifier';
-import { SideNavComponent } from './side-nav/side-nav.component';
 import {VerticalNavComponent} from './vertical-nav/vertical-nav.component';
+import {InlineNotification} from './notification/inline-notification-component';
+
+import { VerticalNavigationModule } from 'patternfly-ng/navigation';
+import {InlineNotificationModule} from 'patternfly-ng/notification/inline-notification';
 
-import { NavigationModule } from 'patternfly-ng/navigation';
 
 /* Routing Module */
 import { AppRoutingModule } from './app-routing.module';
 
 const decs = [
     AppComponent,
-    TopNavComponent,
-    NotificationComponent,
-    SideNavComponent,
     VerticalNavComponent,
+    InlineNotification,
 ];
 
 export const ORIGINAL_INCOMING_URL: Location = window.location;
@@ -62,7 +60,8 @@ export const ORIGINAL_INCOMING_URL: Location = window.location;
     BrowserModule,
     FormsModule,
     HttpModule,
-    NavigationModule,
+    VerticalNavigationModule,
+    InlineNotificationModule,
     TranslateModule.forRoot({
         loader: {provide: TranslateLoader, useClass: DeclaredVarTranslateLoader}
     }),
@@ -73,9 +72,9 @@ export const ORIGINAL_INCOMING_URL: Location = window.location;
     KeycloakGuard,
     KEYCLOAK_HTTP_PROVIDER,
     ResponsivenessService,
+    KeycloakNotificationService,
     AccountServiceClient,
     TranslateUtil,
-    ToastNotifier,
     { provide: LocationStrategy, useClass: HashLocationStrategy }
   ],
   bootstrap: [AppComponent]
diff --git a/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/password-page/password-page.component.html b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/password-page/password-page.component.html
index c10d780..399c2c1 100644
--- a/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/password-page/password-page.component.html
+++ b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/password-page/password-page.component.html
@@ -27,23 +27,23 @@
             <input readonly="" value="this is not a login form" style="display: none;" type="password">
             <div class="form-group">
                 <label for="password" class="control-label">{{'currentPassword' | translate}}</label><span class="required">*</span>
-                <input ngModel class="form-control" #password id="password" name="currentPassword" autofocus="" autocomplete="off" type="password">
+                <input ngModel required class="form-control" #password id="password" name="currentPassword" autofocus="" autocomplete="off" type="password">
             </div>
 
             <div class="form-group">
                 <label for="password-new" class="control-label">{{'passwordNew' | translate}}</label><span class="required">*</span>
-                <input ngModel class="form-control" id="newPassword" name="newPassword" autocomplete="off" type="password">
+                <input ngModel required class="form-control" id="newPassword" name="newPassword" autocomplete="off" type="password">
             </div>
 
             <div class="form-group">
                 <label for="password-confirm" class="control-label">{{'passwordConfirm' | translate}}</label><span class="required">*</span>
-                <input ngModel class="form-control" id="confirmation" name="confirmation" autocomplete="off" type="password">
+                <input ngModel required class="form-control" id="confirmation" name="confirmation" autocomplete="off" type="password">
             </div>
 
             <div class="form-group">
                 <div id="kc-form-buttons" class="submit">
                     <div class="">
-                        <button type="submit" class="btn btn-primary btn-lg" name="submitAction">{{'doSave' | translate}}</button>
+                        <button [disabled]="!formGroup.form.valid" type="submit" class="btn btn-primary btn-lg" name="submitAction">{{'doSave' | translate}}</button>
                     </div>
                 </div>
             </div>
diff --git a/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/password-page/password-page.component.ts b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/password-page/password-page.component.ts
index 177298f..dc2684b 100644
--- a/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/password-page/password-page.component.ts
+++ b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/password-page/password-page.component.ts
@@ -18,7 +18,10 @@ import {Component, OnInit, ViewChild, Renderer2} from '@angular/core';
 import {Response} from '@angular/http';
 import {FormGroup} from '@angular/forms';
 
+import {NotificationType} from 'patternfly-ng/notification';
+
 import {AccountServiceClient} from '../../account-service/account.service';
+import {KeycloakNotificationService} from '../../notification/keycloak-notification.service';
 
 @Component({
     selector: 'app-password-page',
@@ -30,18 +33,34 @@ export class PasswordPageComponent implements OnInit {
     @ViewChild('formGroup') private formGroup: FormGroup;
     private lastPasswordUpdate: number;
     
-    constructor(private accountSvc: AccountServiceClient, private renderer: Renderer2) {
+    constructor(private accountSvc: AccountServiceClient, 
+                private renderer: Renderer2,
+                protected kcNotifySvc: KeycloakNotificationService,) {
         this.accountSvc.doGetRequest("/credentials/password", (res: Response) => this.handleGetResponse(res));
     }
     
     public changePassword() {
         console.log("posting: " + JSON.stringify(this.formGroup.value));
+        if (!this.confirmationMatches()) return;
         this.accountSvc.doPostRequest("/credentials/password", (res: Response) => this.handlePostResponse(res), this.formGroup.value);
         this.renderer.selectRootElement('#password').focus();
     }
     
+    private confirmationMatches(): boolean {
+        const newPassword: string = this.formGroup.value['newPassword'];
+        const confirmation: string = this.formGroup.value['confirmation'];
+        
+        const matches: boolean = newPassword === confirmation;
+        
+        if (!matches) {
+            this.kcNotifySvc.notify('notMatchPasswordMessage', NotificationType.DANGER)
+        }
+        
+        return matches;
+    }
+    
     protected handlePostResponse(res: Response) {
-      console.log('**** response from account POST ***');
+      console.log('**** response from password POST ***');
       console.log(JSON.stringify(res));
       console.log('***************************************');
       this.formGroup.reset();
@@ -49,7 +68,7 @@ export class PasswordPageComponent implements OnInit {
     }
     
     protected handleGetResponse(res: Response) {
-        console.log('**** response from account POST ***');
+        console.log('**** response from password GET ***');
         console.log(JSON.stringify(res));
         console.log('***************************************');
         this.lastPasswordUpdate = res.json()['lastUpdate'];
diff --git a/themes/src/main/resources/theme/keycloak-preview/account/resources/app/ngx-translate/translate.util.ts b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/ngx-translate/translate.util.ts
index 947c2a3..ad833c2 100644
--- a/themes/src/main/resources/theme/keycloak-preview/account/resources/app/ngx-translate/translate.util.ts
+++ b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/ngx-translate/translate.util.ts
@@ -27,13 +27,19 @@ export class TranslateUtil {
     constructor(private translator: TranslateService) {
     }
     
-    public translate(key: string) : string {
+    public translate(key: string, params?: Array<any>): string {
         // remove Freemarker syntax
         if (key.startsWith('${') && key.endsWith('}')) {
             key = key.substring(2, key.length - 1);
         }
         
-        return this.translator.instant(key);
+        const ngTranslateParams = {};
+        for (let i in params) {
+            let paramName: string = 'param_' + i;
+            ngTranslateParams[paramName] = params[i];
+        }
+        
+        return this.translator.instant(key, ngTranslateParams);
     }
 }
 
diff --git a/themes/src/main/resources/theme/keycloak-preview/account/resources/app/notification/inline-notification.component.css b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/notification/inline-notification.component.css
new file mode 100644
index 0000000..18a4765
--- /dev/null
+++ b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/notification/inline-notification.component.css
@@ -0,0 +1,29 @@
+.faux-layout {
+    position: fixed;
+    top: 37px;
+    bottom: 0;
+    left: 0;
+    right: 0;
+    background-color: #f5f5f5;
+    padding-top: 15px;
+    z-index: 1100;
+}
+.example-page-container.container-fluid {
+    position: fixed;
+    top: 37px;
+    bottom: 0;
+    left: 0;
+    right: 0;
+    background-color: #f5f5f5;
+    padding-top: 15px;
+}
+
+.hide-vertical-nav {
+    margin-top: 15px;
+    margin-left: 30px;
+}
+
+.navbar-brand-txt {
+    line-height: 34px;
+}
+
diff --git a/themes/src/main/resources/theme/keycloak-preview/account/resources/app/vertical-nav/vertical-nav.component.html b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/vertical-nav/vertical-nav.component.html
index e11d960..6bc27d1 100644
--- a/themes/src/main/resources/theme/keycloak-preview/account/resources/app/vertical-nav/vertical-nav.component.html
+++ b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/vertical-nav/vertical-nav.component.html
@@ -34,12 +34,11 @@
         </div>
     </pfng-vertical-navigation>
 
-
-
     <div #contentContainer
         class="container-fluid container-cards-pf container-pf-nav-pf-vertical example-page-container">
         <div class="row">
             <div class="col-sm-12">
+                <inline-notification></inline-notification>
                 <router-outlet></router-outlet>
             </div>
         </div>
diff --git a/themes/src/main/resources/theme/keycloak-preview/account/resources/app/vertical-nav/vertical-nav.component.ts b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/vertical-nav/vertical-nav.component.ts
index 3ff0d5b..62a23f2 100644
--- a/themes/src/main/resources/theme/keycloak-preview/account/resources/app/vertical-nav/vertical-nav.component.ts
+++ b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/vertical-nav/vertical-nav.component.ts
@@ -1,107 +1,107 @@
-import {Component, OnInit, ViewEncapsulation, ViewChild} from '@angular/core';
-import {NavigationItemConfig, VerticalNavigationComponent} from 'patternfly-ng/navigation';
-import {TranslateUtil} from '../ngx-translate/translate.util';
-import {KeycloakService} from '../keycloak-service/keycloak.service';
-import {Features} from '../page/features';
-import {Referrer} from "../page/referrer";
-
-declare const baseUrl: string;
-declare const resourceUrl: string;
-declare const features: Features;
-declare const availableLocales: Array<Object>;
-
-@Component({
-    encapsulation: ViewEncapsulation.None,
-    selector: 'vertical-nav',
-    styleUrls: ['./vertical-nav.component.css'],
-    templateUrl: './vertical-nav.component.html'
-})
-export class VerticalNavComponent implements OnInit {
-    @ViewChild('pfVerticalNav') pfVerticalNav: VerticalNavigationComponent;
-
-    public resourceUrl: string = resourceUrl;
-    public availableLocales: Array<Object> = availableLocales;
-
-    private referrer: Referrer;
-
-    navigationItems: NavigationItemConfig[];
-
-    constructor(private keycloakService: KeycloakService,
-        private translateUtil: TranslateUtil, ) {
-        this.referrer = new Referrer(translateUtil);
-    }
-
-    ngOnInit(): void {
-        this.navigationItems = [
-            {
-                title: this.translateUtil.translate('personalInfoHtmlTitle'),
-                iconStyleClass: 'fa fa-user-circle',
-                url: 'account',
-                mobileItem: false
-            },
-            {
-                title: this.translateUtil.translate('accountSecurityTitle'),
-                iconStyleClass: 'fa fa-shield',
-                children: this.makeSecurityChildren(),
-            },
-            {
-                title: this.translateUtil.translate('applicationsHtmlTitle'),
-                iconStyleClass: 'fa fa-th',
-                url: 'applications',
-            }
-        ];
-
-        if (features.isMyResourcesEnabled) {
-            this.navigationItems.push(
-                {
-                    title: this.translateUtil.translate('myResources'),
-                    iconStyleClass: 'fa fa-file-o',
-                    url: 'my-resources',
-                }
-            );
-        }
-    }
-
-    private makeSecurityChildren(): Array<NavigationItemConfig> {
-        const children: Array<NavigationItemConfig> = [
-            {
-                title: this.translateUtil.translate('changePasswordHtmlTitle'),
-                iconStyleClass: 'fa fa-shield',
-                url: 'password',
-            },
-            {
-                title: this.translateUtil.translate('authenticatorTitle'),
-                iconStyleClass: 'fa fa-shield',
-                url: 'authenticator',
-            },
-            {
-                title: this.translateUtil.translate('device-activity'),
-                iconStyleClass: 'fa fa-shield',
-                url: 'device-activity',
-            }
-        ];
-
-        if (features.isLinkedAccountsEnabled) {
-            children.push({
-                title: this.translateUtil.translate('linkedAccountsHtmlTitle'),
-                iconStyleClass: 'fa fa-shield',
-                url: 'linked-accounts',
-            });
-        };
-
-        return children;
-    }
-
-    private logout() {
-        this.keycloakService.logout(baseUrl);
-    }
-
-    private isShowLocales(): boolean {
-        return features.isInternationalizationEnabled && (this.availableLocales.length > 1);
-    }
-
-    private changeLocale(newLocale: string) {
-        this.keycloakService.login({kcLocale: newLocale});
-    }
-
-}
\ No newline at end of file
+import {Component, OnInit, ViewEncapsulation, ViewChild} from '@angular/core';
+import {NavigationItemConfig, VerticalNavigationComponent} from 'patternfly-ng/navigation';
+import {TranslateUtil} from '../ngx-translate/translate.util';
+import {KeycloakService} from '../keycloak-service/keycloak.service';
+import {Features} from '../page/features';
+import {Referrer} from "../page/referrer";
+
+declare const baseUrl: string;
+declare const resourceUrl: string;
+declare const features: Features;
+declare const availableLocales: Array<Object>;
+
+@Component({
+    encapsulation: ViewEncapsulation.None,
+    selector: 'vertical-nav',
+    styleUrls: ['./vertical-nav.component.css'],
+    templateUrl: './vertical-nav.component.html'
+})
+export class VerticalNavComponent implements OnInit {
+    @ViewChild('pfVerticalNav') pfVerticalNav: VerticalNavigationComponent;
+
+    public resourceUrl: string = resourceUrl;
+    public availableLocales: Array<Object> = availableLocales;
+
+    private referrer: Referrer;
+
+    navigationItems: NavigationItemConfig[];
+    
+    constructor(private keycloakService: KeycloakService,
+                private translateUtil: TranslateUtil ) {
+        this.referrer = new Referrer(translateUtil);
+    }
+
+    ngOnInit(): void {
+        this.navigationItems = [
+            {
+                title: this.translateUtil.translate('personalInfoHtmlTitle'),
+                iconStyleClass: 'fa fa-user-circle',
+                url: 'account',
+                mobileItem: false
+            },
+            {
+                title: this.translateUtil.translate('accountSecurityTitle'),
+                iconStyleClass: 'fa fa-shield',
+                children: this.makeSecurityChildren(),
+            },
+            {
+                title: this.translateUtil.translate('applicationsHtmlTitle'),
+                iconStyleClass: 'fa fa-th',
+                url: 'applications',
+            }
+        ];
+
+        if (features.isMyResourcesEnabled) {
+            this.navigationItems.push(
+                {
+                    title: this.translateUtil.translate('myResources'),
+                    iconStyleClass: 'fa fa-file-o',
+                    url: 'my-resources',
+                }
+            );
+        }
+    }
+
+    private makeSecurityChildren(): Array<NavigationItemConfig> {
+        const children: Array<NavigationItemConfig> = [
+            {
+                title: this.translateUtil.translate('changePasswordHtmlTitle'),
+                iconStyleClass: 'fa fa-shield',
+                url: 'password',
+            },
+            {
+                title: this.translateUtil.translate('authenticatorTitle'),
+                iconStyleClass: 'fa fa-shield',
+                url: 'authenticator',
+            },
+            {
+                title: this.translateUtil.translate('device-activity'),
+                iconStyleClass: 'fa fa-shield',
+                url: 'device-activity',
+            }
+        ];
+
+        if (features.isLinkedAccountsEnabled) {
+            children.push({
+                title: this.translateUtil.translate('linkedAccountsHtmlTitle'),
+                iconStyleClass: 'fa fa-shield',
+                url: 'linked-accounts',
+            });
+        };
+
+        return children;
+    }
+
+    private logout() {
+        this.keycloakService.logout(baseUrl);
+    }
+
+    private isShowLocales(): boolean {
+        return features.isInternationalizationEnabled && (this.availableLocales.length > 1);
+    }
+
+    private changeLocale(newLocale: string) {
+        this.keycloakService.login({kcLocale: newLocale});
+    }
+
+}
diff --git a/themes/src/main/resources/theme/keycloak-preview/account/resources/systemjs.config.js b/themes/src/main/resources/theme/keycloak-preview/account/resources/systemjs.config.js
index 60683b9..4b0ae94 100644
--- a/themes/src/main/resources/theme/keycloak-preview/account/resources/systemjs.config.js
+++ b/themes/src/main/resources/theme/keycloak-preview/account/resources/systemjs.config.js
@@ -31,6 +31,9 @@
       // patternfly-ng
       'patternfly-ng/navigation': 'npm:patternfly-ng/bundles/patternfly-ng.umd.min.js',
       'patternfly-ng/utilities': 'npm:patternfly-ng/bundles/patternfly-ng.umd.min.js',
+      'patternfly-ng/notification': 'npm:patternfly-ng/bundles/patternfly-ng.umd.min.js',
+      'patternfly-ng/notification/inline-notification': 'npm:patternfly-ng/bundles/patternfly-ng.umd.min.js',
+      'patternfly-ng/notification/notification-service': 'npm:patternfly-ng/bundles/patternfly-ng.umd.min.js',
       
       // unused patternfly-ng dependencies
       'angular-tree-component': '@empty',
@@ -44,10 +47,6 @@
       'ngx-bootstrap/dropdown': 'npm:ngx-bootstrap/bundles/ngx-bootstrap.umd.min.js',
       'ngx-bootstrap/popover': 'npm:ngx-bootstrap/bundles/ngx-bootstrap.umd.min.js',
       'ngx-bootstrap/tooltip': 'npm:ngx-bootstrap/bundles/ngx-bootstrap.umd.min.js',
-    
-      // patternfly-ng currently requires us to install transpiler.  Need to get rid of this.
-      'plugin-babel': 'npm:systemjs-plugin-babel/plugin-babel.js',
-      'systemjs-babel-build': 'npm:systemjs-plugin-babel/systemjs-babel-browser.js'
     },
     
     bundles: {