diff --git a/guacamole/src/main/webapp/app/manage/controllers/manageSessionsController.js b/guacamole/src/main/webapp/app/manage/controllers/manageSessionsController.js index b4a63c61f..17603e209 100644 --- a/guacamole/src/main/webapp/app/manage/controllers/manageSessionsController.js +++ b/guacamole/src/main/webapp/app/manage/controllers/manageSessionsController.js @@ -29,6 +29,7 @@ angular.module('manage').controller('manageSessionsController', ['$scope', '$inj // Required types var ActiveConnectionWrapper = $injector.get('ActiveConnectionWrapper'); var ConnectionGroup = $injector.get('ConnectionGroup'); + var StableSort = $injector.get('StableSort'); // Required services var activeConnectionService = $injector.get('activeConnectionService'); @@ -37,13 +38,6 @@ angular.module('manage').controller('manageSessionsController', ['$scope', '$inj var guacNotification = $injector.get('guacNotification'); var permissionService = $injector.get('permissionService'); - /** - * The root connection group of the connection group hierarchy. - * - * @type ConnectionGroup - */ - $scope.rootGroup = null; - /** * All permissions associated with the current user, or null if the user's * permissions have not yet been loaded. @@ -60,19 +54,41 @@ angular.module('manage').controller('manageSessionsController', ['$scope', '$inj */ $scope.wrappers = null; - // Query the user's permissions - permissionService.getPermissions(authenticationService.getCurrentUserID()) - .success(function permissionsReceived(permissions) { - $scope.permissions = permissions; - }); + /** + * StableSort instance which maintains the sort order of the visible + * connection wrappers. + * + * @type StableSort + */ + $scope.wrapperOrder = new StableSort([ + 'activeConnection.username', + 'activeConnection.startDate', + 'activeConnection.remoteHost', + 'name' + ]); + + /** + * The root connection group of the connection group hierarchy. + * + * @type ConnectionGroup + */ + var rootGroup = null; + + /** + * All active connections, if known, or null if active connections have not + * yet been loaded. + * + * @type ActiveConnection + */ + var activeConnections = null; /** * Map of all visible connections by object identifier. * * @type Object. */ - $scope.connections = {}; - + var connections = {}; + /** * Map of all currently-selected active connection wrappers by identifier. * @@ -90,7 +106,7 @@ angular.module('manage').controller('manageSessionsController', ['$scope', '$inj var addConnection = function addConnection(connection) { // Add given connection to set of visible connections - $scope.connections[connection.identifier] = connection; + connections[connection.identifier] = connection; }; @@ -113,23 +129,62 @@ angular.module('manage').controller('manageSessionsController', ['$scope', '$inj connectionGroup.childConnectionGroups.forEach(addDescendantConnections); }; - - // Retrieve all connections - connectionGroupService.getConnectionGroupTree(ConnectionGroup.ROOT_IDENTIFIER) - .success(function connectionGroupReceived(rootGroup) { - $scope.rootGroup = rootGroup; - addDescendantConnections($scope.rootGroup); - }); - - // Query active sessions - activeConnectionService.getActiveConnections().success(function sessionsRetrieved(activeConnections) { - + + /** + * Wraps all loaded active connections, storing the resulting array within + * the scope. If required data has not yet finished loading, this function + * has no effect. + */ + var wrapActiveConnections = function wrapActiveConnections() { + + // Abort if not all required data is available + if (!activeConnections || !connections) + return; + // Wrap all active connections for sake of display $scope.wrappers = []; for (var identifier in activeConnections) { - $scope.wrappers.push(new ActiveConnectionWrapper(activeConnections[identifier])); + + var activeConnection = activeConnections[identifier]; + var connection = connections[activeConnection.connectionIdentifier]; + + $scope.wrappers.push(new ActiveConnectionWrapper( + connection.name, + activeConnection + )); + } - + + }; + + // Query the user's permissions + permissionService.getPermissions(authenticationService.getCurrentUserID()) + .success(function permissionsReceived(retrievedPermissions) { + $scope.permissions = retrievedPermissions; + }); + + // Retrieve all connections + connectionGroupService.getConnectionGroupTree(ConnectionGroup.ROOT_IDENTIFIER) + .success(function connectionGroupReceived(retrievedRootGroup) { + + // Load connections from retrieved group tree + rootGroup = retrievedRootGroup; + addDescendantConnections(rootGroup); + + // Attempt to produce wrapped list of active connections + wrapActiveConnections(); + + }); + + // Query active sessions + activeConnectionService.getActiveConnections().success(function sessionsRetrieved(retrievedActiveConnections) { + + // Store received list + activeConnections = retrievedActiveConnections; + + // Attempt to produce wrapped list of active connections + wrapActiveConnections(); + }); /** @@ -141,9 +196,42 @@ angular.module('manage').controller('manageSessionsController', ['$scope', '$inj */ $scope.isLoaded = function isLoaded() { - return $scope.wrappers !== null - && $scope.permissions !== null - && $scope.rootGroup !== null; + return $scope.wrappers !== null + && $scope.permissions !== null; + + }; + + /** + * Returns whether the wrapped session list is sorted by the given + * property. + * + * @param {String} property + * The name of the property to check. + * + * @returns {Boolean} + * true if the wrapped session list is sorted by the given property, + * false otherwise. + */ + $scope.isSortedBy = function isSortedBy(property) { + return $scope.wrapperOrder.primary === property; + }; + + /** + * Sets the primary sorting property to the given property, if not already + * set. If already set, the ascending/descending sort order is toggled. + * + * @param {String} property + * The name of the property to assign as the primary sorting property. + */ + $scope.toggleSort = function toggleSort(property) { + + // Sort in ascending order by new property, if different + if (!$scope.isSortedBy(property)) + $scope.wrapperOrder.reorder(property, false); + + // Otherwise, toggle sort order + else + $scope.wrapperOrder.reorder(property, !$scope.wrapperOrder.descending); }; diff --git a/guacamole/src/main/webapp/app/manage/styles/sessions.css b/guacamole/src/main/webapp/app/manage/styles/sessions.css index 102546183..30b486828 100644 --- a/guacamole/src/main/webapp/app/manage/styles/sessions.css +++ b/guacamole/src/main/webapp/app/manage/styles/sessions.css @@ -31,6 +31,7 @@ .manage table.session-list th { background: rgba(0, 0, 0, 0.125); + font-weight: normal; } .manage table.session-list th, @@ -43,3 +44,35 @@ min-width: 2em; text-align: center; } + +.manage table.session-list th { + cursor: pointer; +} + +.manage table.session-list th.select-session { + cursor: auto; +} + +.manage table.session-list th.sort-primary { + font-weight: bold; + padding-right: 0; +} + +.manage table.session-list th.sort-primary:after { + + display: inline-block; + width: 1em; + height: 1em; + vertical-align: middle; + content: ' '; + + background-size: 1em 1em; + background-position: right center; + background-repeat: no-repeat; + background-image: url('images/arrows/down.png'); + +} + +.manage table.session-list th.sort-primary.sort-descending:after { + background-image: url('images/arrows/up.png'); +} diff --git a/guacamole/src/main/webapp/app/manage/templates/manageSessions.html b/guacamole/src/main/webapp/app/manage/templates/manageSessions.html index 9b5ad2721..d2b29adc8 100644 --- a/guacamole/src/main/webapp/app/manage/templates/manageSessions.html +++ b/guacamole/src/main/webapp/app/manage/templates/manageSessions.html @@ -41,10 +41,22 @@ THE SOFTWARE. - {{'MANAGE_SESSION.TABLE_HEADER_SESSION_USERNAME' | translate}} - {{'MANAGE_SESSION.TABLE_HEADER_SESSION_STARTDATE' | translate}} - {{'MANAGE_SESSION.TABLE_HEADER_SESSION_REMOTEHOST' | translate}} - {{'MANAGE_SESSION.TABLE_HEADER_SESSION_CONNECTION_NAME' | translate}} + + {{'MANAGE_SESSION.TABLE_HEADER_SESSION_USERNAME' | translate}} + + + {{'MANAGE_SESSION.TABLE_HEADER_SESSION_STARTDATE' | translate}} + + + {{'MANAGE_SESSION.TABLE_HEADER_SESSION_REMOTEHOST' | translate}} + + + {{'MANAGE_SESSION.TABLE_HEADER_SESSION_CONNECTION_NAME' | translate}} + @@ -55,7 +67,7 @@ THE SOFTWARE. {{wrapper.activeConnection.username}} {{wrapper.activeConnection.startDate | date:'short'}} {{wrapper.activeConnection.remoteHost}} - {{connections[wrapper.activeConnection.connectionIdentifier].name}} + {{wrapper.name}} @@ -66,7 +78,7 @@ THE SOFTWARE.

- + \ No newline at end of file diff --git a/guacamole/src/main/webapp/app/manage/types/ActiveConnectionWrapper.js b/guacamole/src/main/webapp/app/manage/types/ActiveConnectionWrapper.js index b15a2929b..92130c270 100644 --- a/guacamole/src/main/webapp/app/manage/types/ActiveConnectionWrapper.js +++ b/guacamole/src/main/webapp/app/manage/types/ActiveConnectionWrapper.js @@ -31,10 +31,20 @@ angular.module('manage').factory('ActiveConnectionWrapper', [ * properties, such as a checked option. * * @constructor + * @param {String} name + * The display name of the active connection. + * * @param {ActiveConnection} activeConnection * The ActiveConnection to wrap. */ - var ActiveConnectionWrapper = function ActiveConnectionWrapper(activeConnection) { + var ActiveConnectionWrapper = function ActiveConnectionWrapper(name, activeConnection) { + + /** + * The display name of this connection. + * + * @type String + */ + this.name = name; /** * The wrapped ActiveConnection. diff --git a/guacamole/src/main/webapp/app/manage/types/StableSort.js b/guacamole/src/main/webapp/app/manage/types/StableSort.js new file mode 100644 index 000000000..36c21fc78 --- /dev/null +++ b/guacamole/src/main/webapp/app/manage/types/StableSort.js @@ -0,0 +1,115 @@ +/* + * 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 defining the StableSort class. + */ +angular.module('manage').factory('StableSort', [ + function defineStableSort() { + + /** + * Maintains a sorting predicate as required by the Angular orderBy filter. + * The order of properties sorted by the predicate can be altered while + * otherwise maintaining the sort order. + * + * @constructor + * @param {String[]} predicate + * The properties to sort by, in order of precidence. + */ + var StableSort = function StableSort(predicate) { + + /** + * Reference to this instance. + * + * @type StableSort + */ + var stableSort = this; + + /** + * The current sorting predicate. + * + * @type String[] + */ + this.predicate = predicate; + + /** + * The name of the highest-precedence sorting property. + * + * @type String + */ + this.primary = predicate[0]; + + /** + * Whether the highest-precedence sorting property is sorted in + * descending order. + * + * @type Boolean + */ + this.descending = false; + + // Handle initially-descending primary properties + if (this.primary.charAt(0) === '-') { + this.primary = this.primary.substring(1); + this.descending = true; + } + + /** + * Reorders the currently-defined predicate such that the named + * property takes precidence over all others. The property will be + * sorted in ascending order unless otherwise specified. + * + * @param {String} name + * The name of the property to reorder by. + * + * @param {Boolean} [descending=false] + * Whether the property should be sorted in descending order. By + * default, all properties are sorted in ascending order. + */ + this.reorder = function reorder(name, descending) { + + // Build ascending and descending predicate components + var ascendingName = name; + var descendingName = '-' + name; + + // Remove requested property from current predicate + stableSort.predicate = stableSort.predicate.filter(function notRequestedProperty(current) { + return current !== ascendingName + && current !== descendingName; + }); + + // Add property to beginning of predicate + if (descending) + stableSort.predicate.unshift(descendingName); + else + stableSort.predicate.unshift(ascendingName); + + // Update sorted state + stableSort.primary = name; + stableSort.descending = !!descending; + + }; + + }; + + return StableSort; + +}]); \ No newline at end of file diff --git a/guacamole/src/main/webapp/app/userMenu/styles/user-menu.css b/guacamole/src/main/webapp/app/userMenu/styles/user-menu.css index deb81cc84..9d28a3253 100644 --- a/guacamole/src/main/webapp/app/userMenu/styles/user-menu.css +++ b/guacamole/src/main/webapp/app/userMenu/styles/user-menu.css @@ -127,7 +127,7 @@ background-repeat: no-repeat; background-size: 1em; background-position: center center; - background-image: url('images/action-icons/guac-open-downward.png'); + background-image: url('images/arrows/down.png'); } diff --git a/guacamole/src/main/webapp/images/arrows/arrows-d.png b/guacamole/src/main/webapp/images/arrows/arrows-d.png deleted file mode 100644 index 15b1a77b3..000000000 Binary files a/guacamole/src/main/webapp/images/arrows/arrows-d.png and /dev/null differ diff --git a/guacamole/src/main/webapp/images/arrows/arrows-l.png b/guacamole/src/main/webapp/images/arrows/arrows-l.png deleted file mode 100644 index 91f8150d3..000000000 Binary files a/guacamole/src/main/webapp/images/arrows/arrows-l.png and /dev/null differ diff --git a/guacamole/src/main/webapp/images/arrows/arrows-r.png b/guacamole/src/main/webapp/images/arrows/arrows-r.png deleted file mode 100644 index 3ab9d5b2b..000000000 Binary files a/guacamole/src/main/webapp/images/arrows/arrows-r.png and /dev/null differ diff --git a/guacamole/src/main/webapp/images/arrows/arrows-u.png b/guacamole/src/main/webapp/images/arrows/arrows-u.png deleted file mode 100644 index 057cccf95..000000000 Binary files a/guacamole/src/main/webapp/images/arrows/arrows-u.png and /dev/null differ diff --git a/guacamole/src/main/webapp/images/action-icons/guac-open-downward.png b/guacamole/src/main/webapp/images/arrows/down.png similarity index 100% rename from guacamole/src/main/webapp/images/action-icons/guac-open-downward.png rename to guacamole/src/main/webapp/images/arrows/down.png diff --git a/guacamole/src/main/webapp/images/arrows/up.png b/guacamole/src/main/webapp/images/arrows/up.png new file mode 100644 index 000000000..e751af83d Binary files /dev/null and b/guacamole/src/main/webapp/images/arrows/up.png differ