GUAC-1126 Created user page service which provides the list of main pages for a user.

This commit is contained in:
James Muehlner
2015-04-01 23:21:31 -07:00
committed by Michael Jumper
parent 1d0b32388b
commit 7bf4cb83d8
11 changed files with 314 additions and 210 deletions

View File

@@ -0,0 +1,309 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
/**
* A directive which provides a user-oriented menu containing options for
* navigation and configuration.
*/
angular.module('navigation').directive('guacUserMenu', [function guacUserMenu() {
return {
restrict: 'E',
replace: true,
scope: {
/**
* The permissions associated with the user for whom this menu is
* being displayed.
*
* @type PermissionSet
*/
permissions : '=',
/**
* The root of the connection group tree.
*
* @type ConnectionGroup
*/
rootGroup : '='
},
templateUrl: 'app/navigation/templates/guacUserMenu.html',
controller: ['$scope', '$injector', '$element', function guacUserMenuController($scope, $injector, $element) {
// Get required types
var ConnectionGroup = $injector.get('ConnectionGroup');
var PermissionSet = $injector.get('PermissionSet');
// Get required services
var $document = $injector.get('$document');
var $location = $injector.get('$location');
var authenticationService = $injector.get('authenticationService');
var guacNotification = $injector.get('guacNotification');
var userService = $injector.get('userService');
var userPageService = $injector.get('userPageService');
/**
* An action to be provided along with the object sent to
* showStatus which closes the currently-shown status dialog.
*/
var ACKNOWLEDGE_ACTION = {
name : 'USER_MENU.ACTION_ACKNOWLEDGE',
// Handle action
callback : function acknowledgeCallback() {
guacNotification.showStatus(false);
}
};
/**
* The outermost element of the user menu directive.
*
* @type Element
*/
var element = $element[0];
/**
* The main document object.
*
* @type Document
*/
var document = $document[0];
/**
* Whether the current user has sufficient permissions to change
* his/her own password. If permissions have not yet been loaded,
* this will be null.
*
* @type Boolean
*/
$scope.canChangePassword = null;
/**
* Whether the password edit dialog should be shown.
*
* @type Boolean
*/
$scope.showPasswordDialog = false;
/**
* The new password for the user.
*
* @type String
*/
$scope.newPassword = null;
/**
* The password match for the user. The update password action will
* fail if $scope.newPassword !== $scope.passwordMatch.
*
* @type String
*/
$scope.newPasswordMatch = null;
/**
* Whether the contents of the user menu are currently shown.
*
* @type Boolean
*/
$scope.menuShown = false;
/**
* The username of the current user.
*
* @type String
*/
$scope.username = authenticationService.getCurrentUserID();
/**
* The available main pages for the current user.
*
* @type Page[]
*/
$scope.pages = null;
/**
* Updates the visible menu items based on the permissions and root
* group on the scope, if available. If either the permissions or
* the root group are not yet available, this function has no
* effect.
*/
var updateMenuItems = function updateMenuItems() {
// Menu items are unknown until permissions and rootGroup are both available
if (!$scope.permissions || !$scope.rootGroup) {
$scope.canChangePassword = null;
$scope.pages = [];
return;
}
// Retrieve the main pages from the user page service
$scope.pages = userPageService.getMainPages($scope.rootGroup, $scope.permissions);
// Check whether the current user can change their own password
$scope.canChangePassword = PermissionSet.hasUserPermission(
$scope.permissions, PermissionSet.ObjectPermissionType.UPDATE,
authenticationService.getCurrentUserID()
);
};
// Update available menu options when permissions are changed
$scope.$watch('permissions', function permissionsChanged() {
updateMenuItems();
});
// Update available menu options when root group is changed
$scope.$watch('rootGroup', function rootGroupChanged() {
updateMenuItems();
});
/**
* Toggles visibility of the user menu.
*/
$scope.toggleMenu = function toggleMenu() {
$scope.menuShown = !$scope.menuShown;
};
/**
* Show the password update dialog.
*/
$scope.showPasswordUpdate = function showPasswordUpdate() {
// Show the dialog
$scope.showPasswordDialog = true;
};
/**
* Close the password update dialog.
*/
$scope.closePasswordUpdate = function closePasswordUpdate() {
// Clear the password fields and close the dialog
$scope.oldPassword = null;
$scope.newPassword = null;
$scope.newPasswordMatch = null;
$scope.showPasswordDialog = false;
};
/**
* Update the current user's password to the password currently set within
* the password change dialog.
*/
$scope.updatePassword = function updatePassword() {
// Verify passwords match
if ($scope.newPasswordMatch !== $scope.newPassword) {
guacNotification.showStatus({
className : 'error',
title : 'USER_MENU.DIALOG_HEADER_ERROR',
text : 'USER_MENU.ERROR_PASSWORD_MISMATCH',
actions : [ ACKNOWLEDGE_ACTION ]
});
return;
}
// Verify that the new password is not blank
if (!$scope.newPassword) {
guacNotification.showStatus({
className : 'error',
title : 'USER_MENU.DIALOG_HEADER_ERROR',
text : 'USER_MENU.ERROR_PASSWORD_BLANK',
actions : [ ACKNOWLEDGE_ACTION ]
});
return;
}
// Save the user with the new password
userService.updateUserPassword($scope.username, $scope.oldPassword, $scope.newPassword)
.success(function passwordUpdated() {
// Close the password update dialog
$scope.closePasswordUpdate();
// Indicate that the password has been changed
guacNotification.showStatus({
text : 'USER_MENU.PASSWORD_CHANGED',
actions : [ ACKNOWLEDGE_ACTION ]
});
})
// Notify of any errors
.error(function passwordUpdateFailed(error) {
guacNotification.showStatus({
className : 'error',
title : 'USER_MENU.DIALOG_HEADER_ERROR',
'text' : error.message,
actions : [ ACKNOWLEDGE_ACTION ]
});
});
};
/**
* Navigate to the given page.
*
* @param {Page} page
* The page to navigate to.
*/
$scope.navigateToPage = function navigateToPage(page) {
$location.path(page.url);
};
/**
* Tests whether the given page should be disabled.
*
* @param {Page} page
* The page to test.
*
* @returns {Boolean}
* true if the given page should be disabled, false otherwise.
*/
$scope.isPageDisabled = function isPageDisabled(page) {
return $location.url() === page.url;
};
/**
* Logs out the current user, redirecting them to back to the login
* screen after logout completes.
*/
$scope.logout = function logout() {
authenticationService.logout()['finally'](function logoutComplete() {
$location.path('/login/');
});
};
// Close menu when use clicks anywhere else
document.body.addEventListener('click', function clickOutsideMenu() {
$scope.$apply(function closeMenu() {
$scope.menuShown = false;
});
}, false);
// Prevent click within menu from triggering the outside-menu handler
element.addEventListener('click', function clickInsideMenu(e) {
e.stopPropagation();
}, false);
}] // end controller
};
}]);

View File

@@ -0,0 +1,26 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
/**
* Module for generating and implementing user navigation options.
*/
angular.module('navigation', ['notification']);

View File

@@ -0,0 +1,206 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
/**
* A service for generating all the important pages a user can visit.
*/
angular.module('navigation').factory('userPageService', ['$injector',
function userPageService($injector) {
// Get required types
var ConnectionGroup = $injector.get('ConnectionGroup');
var PermissionSet = $injector.get('PermissionSet');
// Get required services
var authenticationService = $injector.get('authenticationService');
var service = {};
/**
* Construct a new Page object with the given name and url.
* @constructor
*
* @param {String} name
* The i18n key for the name of the page.
*
* @param {String} url
* The url to the page.
*
* @returns {PageDefinition}
* The newly created PageDefinition object.
*/
var Page = function Page(name, url) {
this.name = name;
this.url = url;
};
/**
* Returns an appropriate home page for the current user.
*
* @param {ConnectionGroup} rootGroup
* The root of the connection group tree for the current user.
*
* @returns {Page}
* The user's home page.
*/
service.getHomePage = function getHomePage(rootGroup) {
// Get children
var connections = rootGroup.childConnections || [];
var connectionGroups = rootGroup.childConnectionGroups || [];
// Use main connection list screen as home if multiple connections
// are available
if (connections.length + connectionGroups.length === 1) {
var connection = connections[0];
var connectionGroup = connectionGroups[0];
// Only one connection present, use as home page
if (connection) {
return new Page(
connection.name,
'/client/c/' + connection.identifier
);
}
// Only one connection present, use as home page
if (connectionGroup
&& connectionGroup.type === ConnectionGroup.Type.BALANCING
&& _.isEmpty(connectionGroup.childConnections)
&& _.isEmpty(connectionGroup.childConnectionGroups)) {
return new Page(
connectionGroup.name,
'/client/g/' + connectionGroup.identifier
);
}
}
// Default home page
return new Page(
'USER_MENU.ACTION_NAVIGATE_HOME',
'/'
);
};
/**
* Returns all the main pages that the current user can visit. This can
* include the home page, manage pages, etc. In the case that there are no
* applicable pages of this sort, it may return a client page.
*
* @param {ConnectionGroup} rootGroup
* The root of the connection group tree for the current user.
*
* @param {PermissionSet} permissions
* The permissions for the current user.
*
* @returns {Array}
* An array of objects like this
*/
service.getMainPages = function getMainPages(rootGroup, permissions) {
var pages = [];
permissions = angular.copy(permissions);
// Ignore permission to update root group
PermissionSet.removeConnectionGroupPermission(permissions, PermissionSet.ObjectPermissionType.UPDATE, ConnectionGroup.ROOT_IDENTIFIER);
// Ignore permission to update self
PermissionSet.removeUserPermission(permissions, PermissionSet.ObjectPermissionType.UPDATE, authenticationService.getCurrentUserID());
// Determine whether the current user needs access to the user management UI
var canManageUsers =
// System permissions
PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.ADMINISTER)
|| PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.CREATE_USER)
// Permission to update users
|| PermissionSet.hasUserPermission(permissions, PermissionSet.ObjectPermissionType.UPDATE)
// Permission to delete users
|| PermissionSet.hasUserPermission(permissions, PermissionSet.ObjectPermissionType.DELETE)
// Permission to administer users
|| PermissionSet.hasUserPermission(permissions, PermissionSet.ObjectPermissionType.ADMINISTER);
// Determine whether the current user needs access to the connection management UI
var canManageConnections =
// System permissions
PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.ADMINISTER)
|| PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.CREATE_CONNECTION)
|| PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.CREATE_CONNECTION_GROUP)
// Permission to update connections or connection groups
|| PermissionSet.hasConnectionPermission(permissions, PermissionSet.ObjectPermissionType.UPDATE)
|| PermissionSet.hasConnectionGroupPermission(permissions, PermissionSet.ObjectPermissionType.UPDATE)
// Permission to delete connections or connection groups
|| PermissionSet.hasConnectionPermission(permissions, PermissionSet.ObjectPermissionType.DELETE)
|| PermissionSet.hasConnectionGroupPermission(permissions, PermissionSet.ObjectPermissionType.DELETE)
// Permission to administer connections or connection groups
|| PermissionSet.hasConnectionPermission(permissions, PermissionSet.ObjectPermissionType.ADMINISTER)
|| PermissionSet.hasConnectionGroupPermission(permissions, PermissionSet.ObjectPermissionType.ADMINISTER);
var canManageSessions =
// A user must be a system administrator to manage sessions
PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.ADMINISTER);
// Add home page
pages.push(service.getHomePage(rootGroup));
// If user can manage users, add link to user management page
if (canManageUsers) {
pages.push(new Page(
'USER_MENU.ACTION_MANAGE_USERS',
'/manage/modules/users/'
));
}
// If user can manage connections, add link to connections management page
if (canManageConnections) {
pages.push(new Page(
'USER_MENU.ACTION_MANAGE_CONNECTIONS',
'/manage/modules/connections/'
));
}
// If user can manage sessions, add link to sessions management page
if (canManageSessions) {
pages.push(new Page(
'USER_MENU.ACTION_MANAGE_SESSIONS',
'/manage/modules/sessions/'
));
}
return pages;
};
return service;
}]);

View File

@@ -0,0 +1,258 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
.user-menu {
/* IE10 */
display: -ms-flexbox;
-ms-flex-align: stretch;
-ms-flex-direction: row;
/* Ancient Mozilla */
display: -moz-box;
-moz-box-align: stretch;
-moz-box-orient: horizontal;
/* Ancient WebKit */
display: -webkit-box;
-webkit-box-align: stretch;
-webkit-box-orient: horizontal;
/* Old WebKit */
display: -webkit-flex;
-webkit-align-items: stretch;
-webkit-flex-direction: row;
/* W3C */
display: flex;
align-items: stretch;
flex-direction: row;
}
.user-menu .user-menu-dropdown {
/* IE10 */
display: -ms-flexbox;
-ms-flex-align: center;
-ms-flex-direction: row;
/* Ancient Mozilla */
display: -moz-box;
-moz-box-align: center;
-moz-box-orient: horizontal;
/* Ancient WebKit */
display: -webkit-box;
-webkit-box-align: center;
-webkit-box-orient: horizontal;
/* Old WebKit */
display: -webkit-flex;
-webkit-align-items: center;
-webkit-flex-direction: row;
/* W3C */
display: flex;
align-items: center;
flex-direction: row;
}
.user-menu .user-menu-dropdown {
position: relative;
border-left: 1px solid rgba(0,0,0,0.125);
background: rgba(0,0,0,0.04);
}
.user-menu .user-menu-dropdown:hover {
background: rgba(0,0,0,0.01);
}
.user-menu .user-menu-dropdown.open,
.user-menu .user-menu-dropdown.open:hover {
background: rgba(0,0,0,0.3);
}
.user-menu .username {
cursor: default;
margin: 0;
min-width: 2in;
font-size: 1.25em;
font-weight: bold;
padding: 0.5em 2em;
background-repeat: no-repeat;
background-size: 1em;
background-position: 0.5em center;
background-image: url('images/user-icons/guac-user.png');
-ms-flex: 0 0 auto;
-moz-box-flex: 0;
-webkit-box-flex: 0;
-webkit-flex: 0 0 auto;
flex: 0 0 auto;
}
.user-menu .menu-indicator {
position: absolute;
right: 0;
top: 0;
bottom: 0;
width: 2em;
background-repeat: no-repeat;
background-size: 1em;
background-position: center center;
background-image: url('images/arrows/down.png');
}
.user-menu .options {
visibility: hidden;
position: absolute;
top: 100%;
right: 0;
left: -1px;
margin: 0;
padding: 0;
background: #EEE;
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.125);
border-left: 1px solid rgba(0,0,0,0.125);
border-bottom: 1px solid rgba(0,0,0,0.125);
z-index: 5;
}
.user-menu .user-menu-dropdown.open .options {
visibility: visible;
}
.user-menu .options li {
padding: 0;
list-style-type: none;
}
.user-menu .options li a {
display: block;
cursor: pointer;
color: black;
text-decoration: none;
padding: 0.75em;
}
.user-menu .options li a:hover {
background-color: #CDA;
}
.user-menu .options li a.disabled,
.user-menu .options li a.disabled:hover {
background-color: transparent;
cursor: default;
opacity: 0.25;
}
.user-menu .options li a.home,
.user-menu .options li a.manage-users,
.user-menu .options li a.manage-connections,
.user-menu .options li a.manage-sessions,
.user-menu .options li a.change-password,
.user-menu .options li a.logout {
background-repeat: no-repeat;
background-size: 1em;
background-position: 0.75em center;
padding-left: 2.5em;
}
.user-menu .options li a[href="#/"] {
background-image: url('images/action-icons/guac-home-dark.png');
}
.user-menu .options li a[href="#/manage/modules/users/"] {
background-image: url('images/user-icons/guac-user.png');
}
.user-menu .options li a[href="#/manage/modules/connections/"] {
background-image: url('images/protocol-icons/guac-monitor.png');
}
.user-menu .options li a[href="#/manage/modules/sessions/"] {
background-image: url('images/protocol-icons/guac-plug.png');
}
.user-menu .options li a.change-password {
background-image: url('images/action-icons/guac-key-dark.png');
}
.user-menu .options li a.logout {
background-image: url('images/action-icons/guac-logout-dark.png');
}
.user-menu .password-dialog {
visibility: hidden;
opacity: 0;
-webkit-transition: visibility 0.125s, opacity 0.125s;
-moz-transition: visibility 0.125s, opacity 0.125s;
-ms-transition: visibility 0.125s, opacity 0.125s;
-o-transition: visibility 0.125s, opacity 0.125s;
transition: visibility 0.125s, opacity 0.125s;
position: absolute;
background: white;
padding: 1em;
border: 1px solid rgba(0, 0, 0, 0.25);
box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.25);
margin: 1em;
right: 0;
top: 0;
z-index: 8;
}
.user-menu .password-dialog .fields {
text-align: right;
}
.user-menu .password-dialog .action-buttons {
text-align: center;
margin: 0;
}
.user-menu .password-dialog.shown {
visibility: visible;
opacity: 1;
-webkit-transition: opacity 0.125s;
-moz-transition: opacity 0.125s;
-ms-transition: opacity 0.125s;
-o-transition: opacity 0.125s;
transition: opacity 0.125s;
}
.user-menu .password-dialog .fields {
width: 100%;
}

View File

@@ -0,0 +1,84 @@
<div class="user-menu">
<!--
Copyright (C) 2015 Glyptodon LLC
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
-->
<div class="user-menu-dropdown" ng-class="{open: menuShown}" ng-click="toggleMenu()">
<div class="username">{{username}}</div>
<div class="menu-indicator"></div>
<!-- Menu options -->
<ul class="options">
<!-- Navigation links -->
<li ng-repeat="page in pages">
<a class="home" ng-click="navigateToPage(page)"
ng-class="{disabled: isPageDisabled(page)}" href="#{{page.url}}">
{{page.name | translate}}
</a>
</li>
<!-- Change password -->
<li>
<a class="change-password" ng-click="showPasswordUpdate()"
ng-show="canChangePassword">
{{'USER_MENU.ACTION_CHANGE_PASSWORD' | translate}}
</a>
</li>
<!-- Logout -->
<li>
<a class="logout" ng-click="logout()">
{{'USER_MENU.ACTION_LOGOUT' | translate}}
</a>
</li>
</ul>
</div>
<!-- Password dialog -->
<div class="password-dialog" ng-class="{shown: showPasswordDialog}">
<!-- Password editor -->
<div class="section">
<table class="fields">
<tr>
<th>{{'USER_MENU.FIELD_HEADER_PASSWORD_OLD' | translate}}</th>
<td><input ng-model="oldPassword" type="password" /></td>
</tr>
<tr>
<th>{{'USER_MENU.FIELD_HEADER_PASSWORD_NEW' | translate}}</th>
<td><input ng-model="newPassword" type="password" /></td>
</tr>
<tr>
<th>{{'USER_MENU.FIELD_HEADER_PASSWORD_NEW_AGAIN' | translate}}</th>
<td><input ng-model="newPasswordMatch" type="password" /></td>
</tr>
</table>
</div>
<!-- Form action buttons -->
<div class="action-buttons">
<button ng-click="updatePassword()">{{'USER_MENU.ACTION_SAVE' | translate}}</button>
<button ng-click="closePasswordUpdate()">{{'USER_MENU.ACTION_CANCEL' | translate}}</button>
</div>
</div>
</div>