KEYCLOAK-7857: Fix notifications

This commit is contained in:
ssilvert@win.redhat.com 2018-07-23 16:04:59 -04:00 committed by Stan Silvert
parent d73c4288ae
commit 0844aa8d68
26 changed files with 224 additions and 639 deletions

View file

@ -22,6 +22,7 @@ package org.keycloak.representations.idm;
*/ */
public class ErrorRepresentation { public class ErrorRepresentation {
private String errorMessage; private String errorMessage;
private Object[] params;
public ErrorRepresentation() { public ErrorRepresentation() {
} }
@ -33,4 +34,12 @@ public class ErrorRepresentation {
public void setErrorMessage(String errorMessage) { public void setErrorMessage(String errorMessage) {
this.errorMessage = errorMessage; this.errorMessage = errorMessage;
} }
public Object[] getParams() {
return this.params;
}
public void setParams(Object[] params) {
this.params = params;
}
} }

View file

@ -32,8 +32,13 @@ public class ErrorResponse {
} }
public static Response error(String message, Response.Status status) { 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(); ErrorRepresentation error = new ErrorRepresentation();
error.setErrorMessage(message); error.setErrorMessage(message);
error.setParams(params);
return Response.status(status).entity(error).type(MediaType.APPLICATION_JSON).build(); return Response.status(status).entity(error).type(MediaType.APPLICATION_JSON).build();
} }

View file

@ -20,6 +20,8 @@ import javax.ws.rs.POST;
import javax.ws.rs.Path; import javax.ws.rs.Path;
import javax.ws.rs.Produces; import javax.ws.rs.Produces;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import org.keycloak.models.ModelException;
import org.keycloak.services.messages.Messages;
public class AccountCredentialResource { public class AccountCredentialResource {
@ -62,10 +64,14 @@ public class AccountCredentialResource {
UserCredentialModel cred = UserCredentialModel.password(update.getCurrentPassword()); UserCredentialModel cred = UserCredentialModel.password(update.getCurrentPassword());
if (!session.userCredentialManager().isValid(realm, user, cred)) { if (!session.userCredentialManager().isValid(realm, user, cred)) {
event.error(org.keycloak.events.Errors.INVALID_USER_CREDENTIALS); 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(); return Response.ok().build();
} }

View file

@ -54,6 +54,7 @@ import javax.ws.rs.core.UriInfo;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import org.keycloak.services.messages.Messages;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -146,27 +147,27 @@ public class AccountRestService {
if (usernameChanged) { if (usernameChanged) {
UserModel existing = session.users().getUserByUsername(userRep.getUsername(), realm); UserModel existing = session.users().getUserByUsername(userRep.getUsername(), realm);
if (existing != null) { if (existing != null) {
return ErrorResponse.exists(Errors.USERNAME_EXISTS); return ErrorResponse.exists(Messages.USERNAME_EXISTS);
} }
user.setUsername(userRep.getUsername()); user.setUsername(userRep.getUsername());
} }
} else if (usernameChanged) { } 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()); boolean emailChanged = userRep.getEmail() != null && !userRep.getEmail().equals(user.getEmail());
if (emailChanged && !realm.isDuplicateEmailsAllowed()) { if (emailChanged && !realm.isDuplicateEmailsAllowed()) {
UserModel existing = session.users().getUserByEmail(userRep.getEmail(), realm); UserModel existing = session.users().getUserByEmail(userRep.getEmail(), realm);
if (existing != null) { if (existing != null) {
return ErrorResponse.exists(Errors.EMAIL_EXISTS); return ErrorResponse.exists(Messages.EMAIL_EXISTS);
} }
} }
if (realm.isRegistrationEmailAsUsername() && !realm.isDuplicateEmailsAllowed()) { if (realm.isRegistrationEmailAsUsername() && !realm.isDuplicateEmailsAllowed()) {
UserModel existing = session.users().getUserByUsername(userRep.getEmail(), realm); UserModel existing = session.users().getUserByUsername(userRep.getEmail(), realm);
if (existing != null) { 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(); return Cors.add(request, Response.ok()).auth().allowedOrigins(auth.getToken()).build();
} catch (ReadOnlyException e) { } 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);
} }
} }

View file

@ -1,30 +0,0 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.services.resources.account;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class Errors {
public static final String USERNAME_EXISTS = "username_exists";
public static final String EMAIL_EXISTS = "email_exists";
public static final String READ_ONLY_USER = "user_read_only";
public static final String READ_ONLY_USERNAME = "username_read_only";
public static final String INVALID_CREDENTIALS = "invalid_credentials";
}

View file

@ -41,6 +41,7 @@ import java.util.List;
import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.junit.Assert.*; import static org.junit.Assert.*;
import org.keycloak.services.messages.Messages;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -119,7 +120,7 @@ public class AccountRestServiceTest extends AbstractTestRealmKeycloakTest {
assertEquals("bobby@localhost", user.getEmail()); assertEquals("bobby@localhost", user.getEmail());
user.setEmail("john-doh@localhost"); user.setEmail("john-doh@localhost");
updateError(user, 409, "email_exists"); updateError(user, 409, Messages.EMAIL_EXISTS);
user.setEmail("test-user@localhost"); user.setEmail("test-user@localhost");
user = updateAndGet(user); user = updateAndGet(user);
@ -131,7 +132,7 @@ public class AccountRestServiceTest extends AbstractTestRealmKeycloakTest {
assertEquals("updatedusername", user.getUsername()); assertEquals("updatedusername", user.getUsername());
user.setUsername("john-doh@localhost"); user.setUsername("john-doh@localhost");
updateError(user, 409, "username_exists"); updateError(user, 409, Messages.USERNAME_EXISTS);
user.setUsername("test-user@localhost"); user.setUsername("test-user@localhost");
user = updateAndGet(user); user = updateAndGet(user);
@ -142,7 +143,7 @@ public class AccountRestServiceTest extends AbstractTestRealmKeycloakTest {
adminClient.realm("test").update(realmRep); adminClient.realm("test").update(realmRep);
user.setUsername("updatedUsername2"); user.setUsername("updatedUsername2");
updateError(user, 400, "username_read_only"); updateError(user, 400, Messages.READ_ONLY_USERNAME);
} }
private UserRepresentation updateAndGet(UserRepresentation user) throws IOException { private UserRepresentation updateAndGet(UserRepresentation user) throws IOException {

View file

@ -18,8 +18,10 @@
import {Injectable} from '@angular/core'; import {Injectable} from '@angular/core';
import {Http, Response, RequestOptionsArgs} from '@angular/http'; 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 {KeycloakService} from '../keycloak-service/keycloak.service';
import {NotificationType} from 'patternfly-ng/notification';
/** /**
* *
@ -31,8 +33,8 @@ export class AccountServiceClient {
private accountUrl: string; private accountUrl: string;
constructor(protected http: Http, constructor(protected http: Http,
protected kcSvc: KeycloakService, protected kcSvc: KeycloakService,
protected notifier: ToastNotifier) { protected kcNotifySvc: KeycloakNotificationService) {
this.accountUrl = kcSvc.authServerUrl() + 'realms/' + kcSvc.realm() + '/account'; this.accountUrl = kcSvc.authServerUrl() + 'realms/' + kcSvc.realm() + '/account';
} }
@ -56,7 +58,7 @@ export class AccountServiceClient {
private handleAccountUpdated(responseHandler: Function, res: Response, successMessage?: string) { private handleAccountUpdated(responseHandler: Function, res: Response, successMessage?: string) {
let message: string = "Your account has been updated."; let message: string = "Your account has been updated.";
if (successMessage) message = successMessage; if (successMessage) message = successMessage;
this.notifier.emit(new ToastNotification(message, "success")); this.kcNotifySvc.notify(message, NotificationType.SUCCESS);
responseHandler(res); responseHandler(res);
} }
@ -102,8 +104,8 @@ export class AccountServiceClient {
if (not500Error && response.json().hasOwnProperty('error_description')) { if (not500Error && response.json().hasOwnProperty('error_description')) {
message = response.json().error_description; message = response.json().error_description;
} }
this.notifier.emit(new ToastNotification(message, "error")); this.kcNotifySvc.notify(message, NotificationType.DANGER, response.json().params);
} }
} }

View file

@ -28,30 +28,28 @@ import { KeycloakService } from './keycloak-service/keycloak.service';
import { KEYCLOAK_HTTP_PROVIDER } from './keycloak-service/keycloak.http'; import { KEYCLOAK_HTTP_PROVIDER } from './keycloak-service/keycloak.http';
import {KeycloakGuard} from './keycloak-service/keycloak.guard'; 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 { AccountServiceClient } from './account-service/account.service';
import {TranslateUtil} from './ngx-translate/translate.util'; import {TranslateUtil} from './ngx-translate/translate.util';
import { DeclaredVarTranslateLoader } from './ngx-translate/declared.var.translate.loader'; import { DeclaredVarTranslateLoader } from './ngx-translate/declared.var.translate.loader';
import { AppComponent } from './app.component'; 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 {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 */ /* Routing Module */
import { AppRoutingModule } from './app-routing.module'; import { AppRoutingModule } from './app-routing.module';
const decs = [ const decs = [
AppComponent, AppComponent,
TopNavComponent,
NotificationComponent,
SideNavComponent,
VerticalNavComponent, VerticalNavComponent,
InlineNotification,
]; ];
export const ORIGINAL_INCOMING_URL: Location = window.location; export const ORIGINAL_INCOMING_URL: Location = window.location;
@ -62,7 +60,8 @@ export const ORIGINAL_INCOMING_URL: Location = window.location;
BrowserModule, BrowserModule,
FormsModule, FormsModule,
HttpModule, HttpModule,
NavigationModule, VerticalNavigationModule,
InlineNotificationModule,
TranslateModule.forRoot({ TranslateModule.forRoot({
loader: {provide: TranslateLoader, useClass: DeclaredVarTranslateLoader} loader: {provide: TranslateLoader, useClass: DeclaredVarTranslateLoader}
}), }),
@ -73,9 +72,9 @@ export const ORIGINAL_INCOMING_URL: Location = window.location;
KeycloakGuard, KeycloakGuard,
KEYCLOAK_HTTP_PROVIDER, KEYCLOAK_HTTP_PROVIDER,
ResponsivenessService, ResponsivenessService,
KeycloakNotificationService,
AccountServiceClient, AccountServiceClient,
TranslateUtil, TranslateUtil,
ToastNotifier,
{ provide: LocationStrategy, useClass: HashLocationStrategy } { provide: LocationStrategy, useClass: HashLocationStrategy }
], ],
bootstrap: [AppComponent] bootstrap: [AppComponent]

View file

@ -27,23 +27,23 @@
<input readonly="" value="this is not a login form" style="display: none;" type="password"> <input readonly="" value="this is not a login form" style="display: none;" type="password">
<div class="form-group"> <div class="form-group">
<label for="password" class="control-label">{{'currentPassword' | translate}}</label><span class="required">*</span> <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>
<div class="form-group"> <div class="form-group">
<label for="password-new" class="control-label">{{'passwordNew' | translate}}</label><span class="required">*</span> <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>
<div class="form-group"> <div class="form-group">
<label for="password-confirm" class="control-label">{{'passwordConfirm' | translate}}</label><span class="required">*</span> <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>
<div class="form-group"> <div class="form-group">
<div id="kc-form-buttons" class="submit"> <div id="kc-form-buttons" class="submit">
<div class=""> <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> </div>
</div> </div>

View file

@ -18,7 +18,10 @@ import {Component, OnInit, ViewChild, Renderer2} from '@angular/core';
import {Response} from '@angular/http'; import {Response} from '@angular/http';
import {FormGroup} from '@angular/forms'; import {FormGroup} from '@angular/forms';
import {NotificationType} from 'patternfly-ng/notification';
import {AccountServiceClient} from '../../account-service/account.service'; import {AccountServiceClient} from '../../account-service/account.service';
import {KeycloakNotificationService} from '../../notification/keycloak-notification.service';
@Component({ @Component({
selector: 'app-password-page', selector: 'app-password-page',
@ -30,18 +33,34 @@ export class PasswordPageComponent implements OnInit {
@ViewChild('formGroup') private formGroup: FormGroup; @ViewChild('formGroup') private formGroup: FormGroup;
private lastPasswordUpdate: number; 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)); this.accountSvc.doGetRequest("/credentials/password", (res: Response) => this.handleGetResponse(res));
} }
public changePassword() { public changePassword() {
console.log("posting: " + JSON.stringify(this.formGroup.value)); 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.accountSvc.doPostRequest("/credentials/password", (res: Response) => this.handlePostResponse(res), this.formGroup.value);
this.renderer.selectRootElement('#password').focus(); 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) { protected handlePostResponse(res: Response) {
console.log('**** response from account POST ***'); console.log('**** response from password POST ***');
console.log(JSON.stringify(res)); console.log(JSON.stringify(res));
console.log('***************************************'); console.log('***************************************');
this.formGroup.reset(); this.formGroup.reset();
@ -49,7 +68,7 @@ export class PasswordPageComponent implements OnInit {
} }
protected handleGetResponse(res: Response) { protected handleGetResponse(res: Response) {
console.log('**** response from account POST ***'); console.log('**** response from password GET ***');
console.log(JSON.stringify(res)); console.log(JSON.stringify(res));
console.log('***************************************'); console.log('***************************************');
this.lastPasswordUpdate = res.json()['lastUpdate']; this.lastPasswordUpdate = res.json()['lastUpdate'];

View file

@ -27,13 +27,19 @@ export class TranslateUtil {
constructor(private translator: TranslateService) { constructor(private translator: TranslateService) {
} }
public translate(key: string) : string { public translate(key: string, params?: Array<any>): string {
// remove Freemarker syntax // remove Freemarker syntax
if (key.startsWith('${') && key.endsWith('}')) { if (key.startsWith('${') && key.endsWith('}')) {
key = key.substring(2, key.length - 1); 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);
} }
} }

View file

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

View file

@ -1,33 +0,0 @@
/*
* Copyright 2017 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Icon} from "./icon";
export type Active = "active" | "";
export class SideNavItem {
constructor(public displayName: string,
public link: string,
public tooltip?: string,
public icon?: Icon,
public active?: Active) {
}
setActive(active: Active) {
this.active = active;
}
}

View file

@ -1,71 +0,0 @@
<nav class="nav-pf-vertical {{this.sideNavClasses}}"> <!-- {{this.sideNavClasses}} collapsed hidden show-mobile-nav -->
<ul class="list-group">
<li *ngFor="let item of navItems" class="list-group-item {{item.active}}">
<a [routerLink]="[item.link]">
<span class="{{item.icon.getClasses()}}" title="{{item.tooltip}}" data-toggle="tooltip" data-placement="right"></span>
<span class="list-group-item-value">{{item.displayName}}</span>
</a>
</li>
<li *ngIf="referrer.exists()" class="list-group-item hidden-sm hidden-md hidden-lg">
<a href="{{referrer.getUri()}}">
<span class="pficon-arrow" title="{{'backTo' | translate:referrer.getName()}}"></span>
<span class="list-group-item-value">{{'backTo' | translate:referrer.getName()}}</span>
</a>
</li>
<li class="list-group-item hidden-sm hidden-md hidden-lg">
<a href="#" (click)="logout()">
<span class="fa fa-sign-out" title="{{'doSignOut' | translate}}"></span>
<span class="list-group-item-value">{{'doSignOut' | translate}}</span>
</a>
</li>
</ul>
</nav>
<!-- <li class="list-group-item active">
<a href="#">
<span class="pficon pficon-user" title="Dashboard" data-toggle="tooltip" data-placement="right"></span>
<span class="list-group-item-value">Personal information</span>
</a>
</li>
<li class="list-group-item">
<a href="#">
<span class="fa fa-link" title="My Services" data-toggle="tooltip" data-placement="right"></span>
<span class="list-group-item-value">Connected Devices</span>
</a>
</li>
<li class="list-group-item">
<a href="#">
<span class="fa fa-cubes" title="My Requests" data-toggle="tooltip" data-placement="right"></span>
<span class="list-group-item-value">Applications</span>
</a>
</li>
<li class="list-group-item">
<a href="#">
<span class="pficon pficon-history" title="My Items" data-toggle="tooltip" data-placement="right"></span>
<span class="list-group-item-value">History</span>
</a>
</li>
<li class="list-group-item hidden-sm hidden-md hidden">
<a href="#">
<span class="pficon pficon-help" title="Help"></span>
<span class="list-group-item-value">Help</span>
</a>
</li>
<li class="list-group-item hidden-sm hidden-md hidden-lg">
<a href="#">
<span class="fa fa-info-circle" title="About"></span>
<span class="list-group-item-value">About</span>
</a>
</li>
<li class="list-group-item hidden-sm hidden-md hidden-lg">
<a href="#">
<span class="pficon pficon-user" title="Preferences"></span>
<span class="list-group-item-value">Preferences</span>
</a>
</li>
<li class="list-group-item hidden-sm hidden-md hidden-lg">
<a href="#">
<span class="fa fa-sign-out" title="Log Out"></span>
<span class="list-group-item-value">Log Out</span>
</a>
</li>-->

View file

@ -1,115 +0,0 @@
/*
* Copyright 2017 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Component, OnInit, HostListener} from '@angular/core';
import {Router, NavigationEnd} from '@angular/router';
import {KeycloakService} from '../keycloak-service/keycloak.service';
import {TranslateUtil} from '../ngx-translate/translate.util';
import {SideNavItem, Active} from '../page/side-nav-item';
import {Icon} from '../page/icon';
import {ResponsivenessService, SideNavClasses, MenuClickListener} from "../responsiveness-service/responsiveness.service";
import {Media} from "../responsiveness-service/media";
import {Referrer} from "../page/referrer";
declare const baseUrl: string;
@Component({
selector: 'app-side-nav',
templateUrl: './side-nav.component.html',
styleUrls: ['./side-nav.component.css']
})
export class SideNavComponent implements OnInit, MenuClickListener {
private referrer: Referrer;
private sideNavClasses: SideNavClasses = this.respSvc.calcSideNavWidthClasses();
private isFirstRouterEvent: boolean = true;
public navItems: SideNavItem[];
constructor(private router: Router,
private translateUtil: TranslateUtil,
private respSvc: ResponsivenessService,
private keycloakService: KeycloakService) {
this.referrer = new Referrer(translateUtil);
this.navItems = [
this.makeSideNavItem("account", new Icon("pficon", "user"), "active"),
this.makeSideNavItem("password", new Icon("pficon", "key")),
this.makeSideNavItem("authenticator", new Icon("pficon", "cloud-security")),
this.makeSideNavItem("device-activity", new Icon("fa", "desktop")),
this.makeSideNavItem("sessions", new Icon("fa", "clock-o")),
this.makeSideNavItem("applications", new Icon("fa", "th"))
];
this.router.events.subscribe(value => {
if (value instanceof NavigationEnd) {
const navEnd = value as NavigationEnd;
this.setActive(navEnd.url);
const media: Media = new Media();
if (media.isSmall() && !this.isFirstRouterEvent) {
this.respSvc.menuClicked();
}
this.isFirstRouterEvent = false;
}
});
this.respSvc.addMenuClickListener(this);
// direct navigation such as '#/password'
this.setActive(window.location.hash.substring(1));
}
// use itemName for translate key, link, and tooltip
private makeSideNavItem(itemName: string, icon: Icon, active?: Active): SideNavItem {
const localizedName: string = this.translateUtil.translate(itemName);
return new SideNavItem(localizedName, itemName, localizedName, icon, active);
}
private logout() {
this.keycloakService.logout(baseUrl);
}
public menuClicked(): void {
this.sideNavClasses = this.respSvc.calcSideNavWidthClasses();
}
@HostListener('window:resize', ['$event'])
private onResize(event: any) {
this.sideNavClasses = this.respSvc.calcSideNavWidthClasses();
}
setActive(url: string) {
for (let navItem of this.navItems) {
if (("/" + navItem.link) === url) {
navItem.setActive("active");
} else {
navItem.setActive("");
}
}
if ("/" === url) {
this.navItems[0].setActive("active");
}
}
ngOnInit() {
}
}

View file

@ -1,12 +0,0 @@
<div *ngIf="isVisible" class="toast-pf
toast-pf-max-width
toast-pf-top-right
alert
{{notification.alertType}}
alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">
<span class="pficon pficon-close"></span>
</button>
<span class="pficon {{notification.icon}}"></span>
{{notification.message}}
</div>

View file

@ -1,43 +0,0 @@
/*
* Copyright 2017 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Component} from '@angular/core';
import {ToastNotifier, ToastNotification} from './toast.notifier';
@Component({
selector: 'notification',
templateUrl: './notification.component.html',
styleUrls: ['./notification.component.css']
})
export class NotificationComponent {
private isVisible: boolean = false;
private notification: ToastNotification = new ToastNotification("");
constructor(toastNotifier: ToastNotifier) {
toastNotifier.subscribe((notification: ToastNotification) => {
this.notification = notification;
this.isVisible = true;
setTimeout(() => {
this.isVisible = false;
}, 8000);
})
}
}

View file

@ -1,75 +0,0 @@
/*
* Copyright 2017 Red Hat Inc. and/or its affiliates and other contributors
* as indicated by the @author tags. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
import {Injectable, EventEmitter} from '@angular/core';
/**
*
* @author Stan Silvert ssilvert@redhat.com (C) 2017 Red Hat Inc.
*/
@Injectable()
export class ToastNotifier extends EventEmitter<ToastNotification> {
constructor() {
super();
}
}
type ToastIcon = "pficon-ok" |
"pficon-info" |
"pficon-warning-triangle-o" |
"pficon-error-circle-o";
type ToastAlertType = "alert-success" |
"alert-info" |
"alert-warning" |
"alert-danger";
export type MessageType = "success" |
"info" |
"warning" |
"error";
export class ToastNotification {
public alertType: ToastAlertType = "alert-success";
public icon: ToastIcon = "pficon-ok";
constructor(public message: string, messageType?: MessageType) {
switch (messageType) {
case "info": {
this.alertType = "alert-info";
this.icon = "pficon-info";
break;
}
case "warning": {
this.alertType = "alert-warning";
this.icon = "pficon-warning-triangle-o";
break;
}
case "error": {
this.alertType = "alert-danger";
this.icon = "pficon-error-circle-o";
break;
}
default: {
this.alertType = "alert-success";
this.icon = "pficon-ok";
}
}
}
}

View file

@ -1,42 +0,0 @@
<!-- Top Nav -->
<nav class="navbar navbar-pf-alt">
<notification></notification>
<div class="navbar-header">
<button *ngIf="keycloakService.authenticated() && showSideNav" (click)="menuClicked()" type="button" class="navbar-toggle">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a href="http://www.keycloak.org" class="navbar-brand">
<img class="navbar-brand-icon" type="image/svg+xml" src="{{resourceUrl}}/app/assets/img/keycloak-logo-min.png" alt="" width="auto" height="30px"/>
</a>
</div>
<nav class="collapse navbar-collapse">
<ul class="nav navbar-nav">
</ul>
<ul class="nav navbar-nav navbar-right navbar-iconic">
<li *ngIf="referrer.exists()">
<a class="nav-item-iconic" href="{{referrer.getUri()}}"><span class="pficon-arrow"></span> {{'backTo' | translate:referrer.getName()}}</a>
</li>
<li class="dropdown" >
<a class="dropdown-toggle nav-item-iconic" id="dropdownMenu2" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
<span title="Username" class="fa pficon-user"></span>
<span class="caret"></span>
</a>
<ul class="dropdown-menu" aria-labelledby="dropdownMenu2">
<li><a href="#" (click)="logout()">{{'doSignOut' | translate}}</a></li>
<li *ngIf="showLocales()" class="dropdown-submenu pull-left">
<a class="test" tabindex="-1" href="#">Change language</a>
<ul class="dropdown-menu">
<li *ngFor="let locale of availableLocales" (click)="changeLocale(locale.locale)"><a tabindex="-1" href="#">{{ locale.label }}</a></li>
</ul>
</li>
</ul>
</li>
</ul>
</nav>
</nav> <!--/.navbar-->

View file

@ -1,69 +0,0 @@
/*
* Copyright 2017 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Component, OnInit, Input} from '@angular/core';
import {TranslateUtil} from '../ngx-translate/translate.util';
import {KeycloakService} from '../keycloak-service/keycloak.service';
import {ResponsivenessService} from "../responsiveness-service/responsiveness.service";
import {Features} from '../page/features';
import {Referrer} from "../page/referrer";
declare const resourceUrl: string;
declare const baseUrl: string;
declare const referrer: string;
declare const referrer_uri: string;
declare const features: Features;
declare const availableLocales: Array<Object>;
@Component({
selector: 'app-top-nav',
templateUrl: './top-nav.component.html',
styleUrls: ['./top-nav.component.css']
})
export class TopNavComponent implements OnInit {
@Input() showSideNav: String;
public resourceUrl: string = resourceUrl;
public availableLocales: Array<Object> = availableLocales;
private referrer: Referrer;
constructor(private keycloakService: KeycloakService,
translateUtil: TranslateUtil,
private respSvc: ResponsivenessService) {
this.referrer = new Referrer(translateUtil);
}
private menuClicked(): void {
this.respSvc.menuClicked();
}
ngOnInit() {
}
private logout() {
this.keycloakService.logout(baseUrl);
}
private showLocales(): boolean {
return features.isInternationalizationEnabled && (this.availableLocales.length > 1);
}
private changeLocale(newLocale: string) {
this.keycloakService.login({kcLocale: newLocale });
}
}

View file

@ -34,12 +34,11 @@
</div> </div>
</pfng-vertical-navigation> </pfng-vertical-navigation>
<div #contentContainer <div #contentContainer
class="container-fluid container-cards-pf container-pf-nav-pf-vertical example-page-container"> class="container-fluid container-cards-pf container-pf-nav-pf-vertical example-page-container">
<div class="row"> <div class="row">
<div class="col-sm-12"> <div class="col-sm-12">
<inline-notification></inline-notification>
<router-outlet></router-outlet> <router-outlet></router-outlet>
</div> </div>
</div> </div>

View file

@ -1,107 +1,107 @@
import {Component, OnInit, ViewEncapsulation, ViewChild} from '@angular/core'; import {Component, OnInit, ViewEncapsulation, ViewChild} from '@angular/core';
import {NavigationItemConfig, VerticalNavigationComponent} from 'patternfly-ng/navigation'; import {NavigationItemConfig, VerticalNavigationComponent} from 'patternfly-ng/navigation';
import {TranslateUtil} from '../ngx-translate/translate.util'; import {TranslateUtil} from '../ngx-translate/translate.util';
import {KeycloakService} from '../keycloak-service/keycloak.service'; import {KeycloakService} from '../keycloak-service/keycloak.service';
import {Features} from '../page/features'; import {Features} from '../page/features';
import {Referrer} from "../page/referrer"; import {Referrer} from "../page/referrer";
declare const baseUrl: string; declare const baseUrl: string;
declare const resourceUrl: string; declare const resourceUrl: string;
declare const features: Features; declare const features: Features;
declare const availableLocales: Array<Object>; declare const availableLocales: Array<Object>;
@Component({ @Component({
encapsulation: ViewEncapsulation.None, encapsulation: ViewEncapsulation.None,
selector: 'vertical-nav', selector: 'vertical-nav',
styleUrls: ['./vertical-nav.component.css'], styleUrls: ['./vertical-nav.component.css'],
templateUrl: './vertical-nav.component.html' templateUrl: './vertical-nav.component.html'
}) })
export class VerticalNavComponent implements OnInit { export class VerticalNavComponent implements OnInit {
@ViewChild('pfVerticalNav') pfVerticalNav: VerticalNavigationComponent; @ViewChild('pfVerticalNav') pfVerticalNav: VerticalNavigationComponent;
public resourceUrl: string = resourceUrl; public resourceUrl: string = resourceUrl;
public availableLocales: Array<Object> = availableLocales; public availableLocales: Array<Object> = availableLocales;
private referrer: Referrer; private referrer: Referrer;
navigationItems: NavigationItemConfig[]; navigationItems: NavigationItemConfig[];
constructor(private keycloakService: KeycloakService, constructor(private keycloakService: KeycloakService,
private translateUtil: TranslateUtil, ) { private translateUtil: TranslateUtil ) {
this.referrer = new Referrer(translateUtil); this.referrer = new Referrer(translateUtil);
} }
ngOnInit(): void { ngOnInit(): void {
this.navigationItems = [ this.navigationItems = [
{ {
title: this.translateUtil.translate('personalInfoHtmlTitle'), title: this.translateUtil.translate('personalInfoHtmlTitle'),
iconStyleClass: 'fa fa-user-circle', iconStyleClass: 'fa fa-user-circle',
url: 'account', url: 'account',
mobileItem: false mobileItem: false
}, },
{ {
title: this.translateUtil.translate('accountSecurityTitle'), title: this.translateUtil.translate('accountSecurityTitle'),
iconStyleClass: 'fa fa-shield', iconStyleClass: 'fa fa-shield',
children: this.makeSecurityChildren(), children: this.makeSecurityChildren(),
}, },
{ {
title: this.translateUtil.translate('applicationsHtmlTitle'), title: this.translateUtil.translate('applicationsHtmlTitle'),
iconStyleClass: 'fa fa-th', iconStyleClass: 'fa fa-th',
url: 'applications', url: 'applications',
} }
]; ];
if (features.isMyResourcesEnabled) { if (features.isMyResourcesEnabled) {
this.navigationItems.push( this.navigationItems.push(
{ {
title: this.translateUtil.translate('myResources'), title: this.translateUtil.translate('myResources'),
iconStyleClass: 'fa fa-file-o', iconStyleClass: 'fa fa-file-o',
url: 'my-resources', url: 'my-resources',
} }
); );
} }
} }
private makeSecurityChildren(): Array<NavigationItemConfig> { private makeSecurityChildren(): Array<NavigationItemConfig> {
const children: Array<NavigationItemConfig> = [ const children: Array<NavigationItemConfig> = [
{ {
title: this.translateUtil.translate('changePasswordHtmlTitle'), title: this.translateUtil.translate('changePasswordHtmlTitle'),
iconStyleClass: 'fa fa-shield', iconStyleClass: 'fa fa-shield',
url: 'password', url: 'password',
}, },
{ {
title: this.translateUtil.translate('authenticatorTitle'), title: this.translateUtil.translate('authenticatorTitle'),
iconStyleClass: 'fa fa-shield', iconStyleClass: 'fa fa-shield',
url: 'authenticator', url: 'authenticator',
}, },
{ {
title: this.translateUtil.translate('device-activity'), title: this.translateUtil.translate('device-activity'),
iconStyleClass: 'fa fa-shield', iconStyleClass: 'fa fa-shield',
url: 'device-activity', url: 'device-activity',
} }
]; ];
if (features.isLinkedAccountsEnabled) { if (features.isLinkedAccountsEnabled) {
children.push({ children.push({
title: this.translateUtil.translate('linkedAccountsHtmlTitle'), title: this.translateUtil.translate('linkedAccountsHtmlTitle'),
iconStyleClass: 'fa fa-shield', iconStyleClass: 'fa fa-shield',
url: 'linked-accounts', url: 'linked-accounts',
}); });
}; };
return children; return children;
} }
private logout() { private logout() {
this.keycloakService.logout(baseUrl); this.keycloakService.logout(baseUrl);
} }
private isShowLocales(): boolean { private isShowLocales(): boolean {
return features.isInternationalizationEnabled && (this.availableLocales.length > 1); return features.isInternationalizationEnabled && (this.availableLocales.length > 1);
} }
private changeLocale(newLocale: string) { private changeLocale(newLocale: string) {
this.keycloakService.login({kcLocale: newLocale}); this.keycloakService.login({kcLocale: newLocale});
} }
} }

View file

@ -31,6 +31,9 @@
// patternfly-ng // patternfly-ng
'patternfly-ng/navigation': 'npm:patternfly-ng/bundles/patternfly-ng.umd.min.js', '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/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 // unused patternfly-ng dependencies
'angular-tree-component': '@empty', 'angular-tree-component': '@empty',
@ -44,10 +47,6 @@
'ngx-bootstrap/dropdown': 'npm:ngx-bootstrap/bundles/ngx-bootstrap.umd.min.js', '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/popover': 'npm:ngx-bootstrap/bundles/ngx-bootstrap.umd.min.js',
'ngx-bootstrap/tooltip': '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: { bundles: {