diff --git a/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js b/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js index 983ae71ad..524b897f2 100644 --- a/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js +++ b/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js @@ -353,11 +353,11 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto var linked = dataSource in users; // Add page entry - $scope.accountPages.push(new PageDefinition( - translationStringService.canonicalize('DATA_SOURCE_' + dataSource) + '.NAME', - '/manage/' + encodeURIComponent(dataSource) + '/users/' + encodeURIComponent(username), - linked ? 'linked' : 'unlinked' - )); + $scope.accountPages.push(new PageDefinition({ + name : translationStringService.canonicalize('DATA_SOURCE_' + dataSource) + '.NAME', + url : '/manage/' + encodeURIComponent(dataSource) + '/users/' + encodeURIComponent(username), + className : linked ? 'linked' : 'unlinked' + })); }); diff --git a/guacamole/src/main/webapp/app/manage/styles/manage-user.css b/guacamole/src/main/webapp/app/manage/styles/manage-user.css index a24d3e569..7d6f077db 100644 --- a/guacamole/src/main/webapp/app/manage/styles/manage-user.css +++ b/guacamole/src/main/webapp/app/manage/styles/manage-user.css @@ -28,14 +28,14 @@ text-transform: none; } -.manage-user .settings-tabs .page-list li.unlinked a[href], -.manage-user .settings-tabs .page-list li.linked a[href] { +.manage-user .page-tabs .page-list li.unlinked a[href], +.manage-user .page-tabs .page-list li.linked a[href] { padding-right: 2.5em; position: relative; } -.manage-user .settings-tabs .page-list li.unlinked a[href]:before, -.manage-user .settings-tabs .page-list li.linked a[href]:before { +.manage-user .page-tabs .page-list li.unlinked a[href]:before, +.manage-user .page-tabs .page-list li.linked a[href]:before { content: ' '; position: absolute; right: 0; @@ -47,19 +47,19 @@ background-position: center; } -.manage-user .settings-tabs .page-list li.unlinked a[href]:before { +.manage-user .page-tabs .page-list li.unlinked a[href]:before { background-image: url('images/plus.png'); } -.manage-user .settings-tabs .page-list li.unlinked a[href] { +.manage-user .page-tabs .page-list li.unlinked a[href] { opacity: 0.5; } -.manage-user .settings-tabs .page-list li.unlinked a[href]:hover, -.manage-user .settings-tabs .page-list li.unlinked a[href].current { +.manage-user .page-tabs .page-list li.unlinked a[href]:hover, +.manage-user .page-tabs .page-list li.unlinked a[href].current { opacity: 1; } -.manage-user .settings-tabs .page-list li.linked a[href]:before { +.manage-user .page-tabs .page-list li.linked a[href]:before { background-image: url('images/checkmark.png'); } diff --git a/guacamole/src/main/webapp/app/manage/templates/manageUser.html b/guacamole/src/main/webapp/app/manage/templates/manageUser.html index 442ee5116..04cd593a6 100644 --- a/guacamole/src/main/webapp/app/manage/templates/manageUser.html +++ b/guacamole/src/main/webapp/app/manage/templates/manageUser.html @@ -27,7 +27,7 @@ THE SOFTWARE.

{{user.username}}

-
+
diff --git a/guacamole/src/main/webapp/app/navigation/directives/guacPageList.js b/guacamole/src/main/webapp/app/navigation/directives/guacPageList.js index bd5f909c5..04ecf7a77 100644 --- a/guacamole/src/main/webapp/app/navigation/directives/guacPageList.js +++ b/guacamole/src/main/webapp/app/navigation/directives/guacPageList.js @@ -33,7 +33,7 @@ angular.module('navigation').directive('guacPageList', [function guacPageList() /** * The array of pages to display. * - * @type Page[] + * @type PageDefinition[] */ pages : '=' @@ -42,13 +42,119 @@ angular.module('navigation').directive('guacPageList', [function guacPageList() templateUrl: 'app/navigation/templates/guacPageList.html', controller: ['$scope', '$injector', function guacPageListController($scope, $injector) { - // Get required services + // Required types + var PageDefinition = $injector.get('PageDefinition'); + + // Required services var $location = $injector.get('$location'); + /** + * The URL of the currently-displayed page. + * + * @type String + */ + var currentURL = $location.url(); + + /** + * The names associated with the current page, if the current page + * is known. The value of this property corresponds to the value of + * PageDefinition.name. Though PageDefinition.name may be a String, + * this will always be an Array. + * + * @type String[] + */ + var currentPageName = []; + + /** + * Array of each level of the page list, where a level is defined + * by a mapping of names (translation strings) to the + * PageDefinitions corresponding to those names. + * + * @type Object.[] + */ + $scope.levels = []; + + /** + * Returns the names associated with the given page, in + * hierarchical order. If the page is only associated with a single + * name, and that name is not stored as an array, it will be still + * be returned as an array containing a single item. + * + * @param {PageDefinition} page + * The page to return the names of. + * + * @return {String[]} + * An array of all names associated with the given page, in + * hierarchical order. + */ + var getPageNames = function getPageNames(page) { + + // If already an array, simply return the name + if (angular.isArray(page.name)) + return page.name; + + // Otherwise, transform into array + return [page.name]; + + }; + + /** + * Adds the given PageDefinition to the overall set of pages + * displayed by this guacPageList, automatically updating the + * available levels ($scope.levels) and the contents of those + * levels. + * + * @param {PageDefinition} page + * The PageDefinition to add. + * + * @param {Number} weight + * The sorting weight to use for the page if it does not + * already have an associated weight. + */ + var addPage = function addPage(page, weight) { + + // Pull all names for page + var names = getPageNames(page); + + // Copy the hierarchy of this page into the displayed levels + // as far as is relevant for the currently-displayed page + for (var i = 0; i < names.length; i++) { + + // Create current level, if it doesn't yet exist + var pages = $scope.levels[i]; + if (!pages) + pages = $scope.levels[i] = {}; + + // Get the name at the current level + var name = names[i]; + + // Determine whether this page definition is part of the + // hierarchy containing the current page + var isCurrentPage = (currentPageName[i] === name); + + // Store new page if it doesn't yet exist at this level + if (!pages[name]) { + pages[name] = new PageDefinition({ + name : name, + url : isCurrentPage ? currentURL : page.url, + className : page.className, + weight : page.weight || weight + }); + } + + // If the name at this level no longer matches the + // hierarchy of the current page, do not go any deeper + if (currentPageName[i] !== name) + break; + + } + + }; + /** * Navigate to the given page. * - * @param {Page} page + * @param {PageDefinition} page * The page to navigate to. */ $scope.navigateToPage = function navigateToPage(page) { @@ -58,16 +164,67 @@ angular.module('navigation').directive('guacPageList', [function guacPageList() /** * Tests whether the given page is the page currently being viewed. * - * @param {Page} page + * @param {PageDefinition} page * The page to test. * * @returns {Boolean} * true if the given page is the current page, false otherwise. */ $scope.isCurrentPage = function isCurrentPage(page) { - return $location.url() === page.url; + return currentURL === page.url; }; + /** + * Given an arbitrary map of PageDefinitions, returns an array of + * those PageDefinitions, sorted by weight. + * + * @param {Object.<*, PageDefinition>} level + * A map of PageDefinitions with arbitrary keys. The value of + * each key is ignored. + * + * @returns {PageDefinition[]} + * An array of all PageDefinitions in the given map, sorted by + * weight. + */ + $scope.getPages = function getPages(level) { + + var pages = []; + + // Convert contents of level to a flat array of pages + angular.forEach(level, function addPageFromLevel(page) { + pages.push(page); + }); + + // Sort page array by weight + pages.sort(function comparePages(a, b) { + return a.weight - b.weight; + }); + + return pages; + + }; + + // Update page levels whenever pages changes + $scope.$watch('pages', function setPages(pages) { + + // Determine current page name + currentPageName = []; + angular.forEach(pages, function findCurrentPageName(page) { + + // If page is current page, store its names + if ($scope.isCurrentPage(page)) + currentPageName = getPageNames(page); + + }); + + // Reset contents of levels + $scope.levels = []; + + // Add all page definitions + angular.forEach(pages, addPage); + + }); + }] // end controller }; diff --git a/guacamole/src/main/webapp/app/navigation/services/userPageService.js b/guacamole/src/main/webapp/app/navigation/services/userPageService.js index 0cb9f530c..ef2280d37 100644 --- a/guacamole/src/main/webapp/app/navigation/services/userPageService.js +++ b/guacamole/src/main/webapp/app/navigation/services/userPageService.js @@ -41,31 +41,16 @@ angular.module('navigation').factory('userPageService', ['$injector', var service = {}; - /** - * Construct a new PageDefinition 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 of the page. - */ - var PageDefinition = function PageDefinition(name, url) { - this.name = name; - this.url = url; - }; - /** * The home page to assign to a user if they can navigate to more than one * page. * * @type PageDefinition */ - var SYSTEM_HOME_PAGE = new PageDefinition( - 'USER_MENU.ACTION_NAVIGATE_HOME', - '/' - ); + var SYSTEM_HOME_PAGE = new PageDefinition({ + name : 'USER_MENU.ACTION_NAVIGATE_HOME', + url : '/' + }); /** * Returns an appropriate home page for the current user. @@ -101,14 +86,14 @@ angular.module('navigation').factory('userPageService', ['$injector', // Only one connection present, use as home page if (connection) { - homePage = new PageDefinition( - connection.name, - '/client/' + ClientIdentifier.toString({ + homePage = new PageDefinition({ + name : connection.name, + url : '/client/' + ClientIdentifier.toString({ dataSource : dataSource, type : ClientIdentifier.Types.CONNECTION, id : connection.identifier }) - ); + }); } // Only one balancing group present, use as home page @@ -116,14 +101,14 @@ angular.module('navigation').factory('userPageService', ['$injector', && connectionGroup.type === ConnectionGroup.Type.BALANCING && _.isEmpty(connectionGroup.childConnections) && _.isEmpty(connectionGroup.childConnectionGroups)) { - homePage = new PageDefinition( - connectionGroup.name, - '/client/' + ClientIdentifier.toString({ + homePage = new PageDefinition({ + name : connectionGroup.name, + url : '/client/' + ClientIdentifier.toString({ dataSource : dataSource, type : ClientIdentifier.Types.CONNECTION_GROUP, id : connectionGroup.identifier }) - ); + }); } } @@ -247,33 +232,33 @@ angular.module('navigation').factory('userPageService', ['$injector', // If user can manage sessions, add link to sessions management page if (canManageSessions) { - pages.push(new PageDefinition( - 'USER_MENU.ACTION_MANAGE_SESSIONS', - '/settings/sessions' - )); + pages.push(new PageDefinition({ + name : 'USER_MENU.ACTION_MANAGE_SESSIONS', + url : '/settings/sessions' + })); } // If user can manage users, add link to user management page if (canManageUsers) { - pages.push(new PageDefinition( - 'USER_MENU.ACTION_MANAGE_USERS', - '/settings/users' - )); + pages.push(new PageDefinition({ + name : 'USER_MENU.ACTION_MANAGE_USERS', + url : '/settings/users' + })); } // If user can manage connections, add link to connections management page if (canManageConnections) { - pages.push(new PageDefinition( - 'USER_MENU.ACTION_MANAGE_CONNECTIONS', - '/settings/connections' - )); + pages.push(new PageDefinition({ + name : 'USER_MENU.ACTION_MANAGE_CONNECTIONS', + url : '/settings/connections' + })); } // Add link to user preferences (always accessible) - pages.push(new PageDefinition( - 'USER_MENU.ACTION_MANAGE_PREFERENCES', - '/settings/preferences' - )); + pages.push(new PageDefinition({ + name : 'USER_MENU.ACTION_MANAGE_PREFERENCES', + url : '/settings/preferences' + })); return pages; }; @@ -338,10 +323,10 @@ angular.module('navigation').factory('userPageService', ['$injector', // Add generic link to the first-available settings page if (settingsPages.length) { - pages.push(new PageDefinition( - 'USER_MENU.ACTION_MANAGE_SETTINGS', - settingsPages[0].url - )); + pages.push(new PageDefinition({ + name : 'USER_MENU.ACTION_MANAGE_SETTINGS', + url : settingsPages[0].url + })); } return pages; diff --git a/guacamole/src/main/webapp/app/navigation/styles/page-tabs.css b/guacamole/src/main/webapp/app/navigation/styles/page-tabs.css new file mode 100644 index 000000000..28b4de263 --- /dev/null +++ b/guacamole/src/main/webapp/app/navigation/styles/page-tabs.css @@ -0,0 +1,58 @@ +/* + * 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. + */ + +.page-tabs .page-list ul { + margin: 0; + padding: 0; + background: rgba(0, 0, 0, 0.0125); + border-bottom: 1px solid rgba(0, 0, 0, 0.05); +} + +.page-tabs .page-list ul + ul { + font-size: 0.75em; +} + +.page-tabs .page-list li { + display: inline-block; + list-style: none; +} + +.page-tabs .page-list li a[href] { + display: block; + color: black; + text-decoration: none; + padding: 0.75em 1em; +} + +.page-tabs .page-list li a[href]:visited { + color: black; +} + +.page-tabs .page-list li a[href]:hover { + background-color: #CDA; +} + +.page-tabs .page-list li a[href].current, +.page-tabs .page-list li a[href].current:hover { + background: rgba(0,0,0,0.3); + cursor: default; +} diff --git a/guacamole/src/main/webapp/app/navigation/templates/guacPageList.html b/guacamole/src/main/webapp/app/navigation/templates/guacPageList.html index 35d12c31e..c5d131e9e 100644 --- a/guacamole/src/main/webapp/app/navigation/templates/guacPageList.html +++ b/guacamole/src/main/webapp/app/navigation/templates/guacPageList.html @@ -1,4 +1,4 @@ - +
diff --git a/guacamole/src/main/webapp/app/navigation/types/PageDefinition.js b/guacamole/src/main/webapp/app/navigation/types/PageDefinition.js index 0b5d00b1e..ba702b014 100644 --- a/guacamole/src/main/webapp/app/navigation/types/PageDefinition.js +++ b/guacamole/src/main/webapp/app/navigation/types/PageDefinition.js @@ -30,37 +30,46 @@ angular.module('navigation').factory('PageDefinition', [function definePageDefin * an arbitrary, human-readable name. * * @constructor - * @param {String} name - * The the name of the page, which should be a translation table key. - * - * @param {String} url - * The URL of the page. - * - * @param {String} [className=''] - * The CSS class name to associate with this page, if any. + * @param {PageDefinition|Object} template + * The object whose properties should be copied within the new + * PageDefinition. */ - var PageDefinition = function PageDefinition(name, url, className) { + var PageDefinition = function PageDefinition(template) { /** * The the name of the page, which should be a translation table key. + * Alternatively, this may also be a list of names, where the final + * name represents the page and earlier names represent categorization. + * Those categorical names may be rendered hierarchically as a system + * of menus, tabs, etc. * - * @type String + * @type String|String[] */ - this.name = name; + this.name = template.name; /** * The URL of the page. * * @type String */ - this.url = url; + this.url = template.url; /** - * The CSS class name to associate with this page, if any. + * The CSS class name to associate with this page, if any. This will be + * an empty string by default. * * @type String */ - this.className = className || ''; + this.className = template.className || ''; + + /** + * A numeric value denoting the relative sort order when compared to + * other sibling PageDefinitions. If unspecified, sort order is + * determined by the system using the PageDefinition. + * + * @type Number + */ + this.weight = template.weight; }; diff --git a/guacamole/src/main/webapp/app/settings/styles/settings.css b/guacamole/src/main/webapp/app/settings/styles/settings.css index 67453a266..deb4b8cc1 100644 --- a/guacamole/src/main/webapp/app/settings/styles/settings.css +++ b/guacamole/src/main/webapp/app/settings/styles/settings.css @@ -34,36 +34,3 @@ text-align: center; margin: 1em 0; } - -.settings-tabs .page-list { - margin: 0; - padding: 0; - background: rgba(0, 0, 0, 0.0125); - border-bottom: 1px solid rgba(0, 0, 0, 0.05); -} - -.settings-tabs .page-list li { - display: inline-block; - list-style: none; -} - -.settings-tabs .page-list li a[href] { - display: block; - color: black; - text-decoration: none; - padding: 0.75em 1em; -} - -.settings-tabs .page-list li a[href]:visited { - color: black; -} - -.settings-tabs .page-list li a[href]:hover { - background-color: #CDA; -} - -.settings-tabs .page-list li a[href].current, -.settings-tabs .page-list li a[href].current:hover { - background: rgba(0,0,0,0.3); - cursor: default; -} diff --git a/guacamole/src/main/webapp/app/settings/templates/settings.html b/guacamole/src/main/webapp/app/settings/templates/settings.html index 3c7f26e84..97142467f 100644 --- a/guacamole/src/main/webapp/app/settings/templates/settings.html +++ b/guacamole/src/main/webapp/app/settings/templates/settings.html @@ -28,7 +28,7 @@ THE SOFTWARE. -
+