Merge pull request #123 from glyptodon/session-sort
GUAC-1140: Implement dynamic sorting of active sessions.
| @@ -29,6 +29,7 @@ angular.module('manage').controller('manageSessionsController', ['$scope', '$inj | |||||||
|     // Required types |     // Required types | ||||||
|     var ActiveConnectionWrapper = $injector.get('ActiveConnectionWrapper'); |     var ActiveConnectionWrapper = $injector.get('ActiveConnectionWrapper'); | ||||||
|     var ConnectionGroup         = $injector.get('ConnectionGroup'); |     var ConnectionGroup         = $injector.get('ConnectionGroup'); | ||||||
|  |     var StableSort              = $injector.get('StableSort'); | ||||||
|  |  | ||||||
|     // Required services |     // Required services | ||||||
|     var activeConnectionService = $injector.get('activeConnectionService'); |     var activeConnectionService = $injector.get('activeConnectionService'); | ||||||
| @@ -37,13 +38,6 @@ angular.module('manage').controller('manageSessionsController', ['$scope', '$inj | |||||||
|     var guacNotification        = $injector.get('guacNotification'); |     var guacNotification        = $injector.get('guacNotification'); | ||||||
|     var permissionService       = $injector.get('permissionService'); |     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 |      * All permissions associated with the current user, or null if the user's | ||||||
|      * permissions have not yet been loaded. |      * permissions have not yet been loaded. | ||||||
| @@ -60,18 +54,40 @@ angular.module('manage').controller('manageSessionsController', ['$scope', '$inj | |||||||
|      */ |      */ | ||||||
|     $scope.wrappers = null; |     $scope.wrappers = null; | ||||||
|  |  | ||||||
|     // Query the user's permissions |     /** | ||||||
|     permissionService.getPermissions(authenticationService.getCurrentUserID()) |      * StableSort instance which maintains the sort order of the visible | ||||||
|             .success(function permissionsReceived(permissions) { |      * connection wrappers. | ||||||
|         $scope.permissions = permissions; |      * | ||||||
|     }); |      * @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. |      * Map of all visible connections by object identifier. | ||||||
|      * |      * | ||||||
|      * @type Object.<String, Connection> |      * @type Object.<String, Connection> | ||||||
|      */ |      */ | ||||||
|     $scope.connections = {}; |     var connections = {}; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Map of all currently-selected active connection wrappers by identifier. |      * 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) { |     var addConnection = function addConnection(connection) { | ||||||
|  |  | ||||||
|         // Add given connection to set of visible connections |         // Add given connection to set of visible connections | ||||||
|         $scope.connections[connection.identifier] = connection; |         connections[connection.identifier] = connection; | ||||||
|  |  | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
| @@ -114,22 +130,61 @@ angular.module('manage').controller('manageSessionsController', ['$scope', '$inj | |||||||
|  |  | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     // Retrieve all connections  |     /** | ||||||
|     connectionGroupService.getConnectionGroupTree(ConnectionGroup.ROOT_IDENTIFIER) |      * Wraps all loaded active connections, storing the resulting array within | ||||||
|     .success(function connectionGroupReceived(rootGroup) { |      * the scope. If required data has not yet finished loading, this function | ||||||
|         $scope.rootGroup = rootGroup; |      * has no effect. | ||||||
|         addDescendantConnections($scope.rootGroup); |      */ | ||||||
|     }); |     var wrapActiveConnections = function wrapActiveConnections() { | ||||||
|  |  | ||||||
|     // Query active sessions |         // Abort if not all required data is available | ||||||
|     activeConnectionService.getActiveConnections().success(function sessionsRetrieved(activeConnections) { |         if (!activeConnections || !connections) | ||||||
|  |             return; | ||||||
|  |  | ||||||
|         // Wrap all active connections for sake of display |         // Wrap all active connections for sake of display | ||||||
|         $scope.wrappers = []; |         $scope.wrappers = []; | ||||||
|         for (var identifier in activeConnections) { |         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(); | ||||||
|  |  | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
| @@ -142,8 +197,41 @@ angular.module('manage').controller('manageSessionsController', ['$scope', '$inj | |||||||
|     $scope.isLoaded = function isLoaded() { |     $scope.isLoaded = function isLoaded() { | ||||||
|  |  | ||||||
|         return $scope.wrappers    !== null |         return $scope.wrappers    !== null | ||||||
|             && $scope.permissions          !== null |             && $scope.permissions !== null; | ||||||
|             && $scope.rootGroup            !== 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); | ||||||
|  |  | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -31,6 +31,7 @@ | |||||||
|  |  | ||||||
| .manage table.session-list th { | .manage table.session-list th { | ||||||
|     background: rgba(0, 0, 0, 0.125); |     background: rgba(0, 0, 0, 0.125); | ||||||
|  |     font-weight: normal; | ||||||
| } | } | ||||||
|  |  | ||||||
| .manage table.session-list th, | .manage table.session-list th, | ||||||
| @@ -43,3 +44,35 @@ | |||||||
|     min-width: 2em; |     min-width: 2em; | ||||||
|     text-align: center; |     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'); | ||||||
|  | } | ||||||
|   | |||||||
| @@ -41,10 +41,22 @@ THE SOFTWARE. | |||||||
|             <thead> |             <thead> | ||||||
|                 <tr> |                 <tr> | ||||||
|                     <th class="select-session"></th> |                     <th class="select-session"></th> | ||||||
|                     <th>{{'MANAGE_SESSION.TABLE_HEADER_SESSION_USERNAME' | translate}}</th> |                     <th ng-class="{'sort-primary': isSortedBy('activeConnection.username'), 'sort-descending': wrapperOrder.descending}" | ||||||
|                     <th>{{'MANAGE_SESSION.TABLE_HEADER_SESSION_STARTDATE' | translate}}</th> |                         ng-click="toggleSort('activeConnection.username')"> | ||||||
|                     <th>{{'MANAGE_SESSION.TABLE_HEADER_SESSION_REMOTEHOST' | translate}}</th> |                         {{'MANAGE_SESSION.TABLE_HEADER_SESSION_USERNAME' | translate}} | ||||||
|                     <th>{{'MANAGE_SESSION.TABLE_HEADER_SESSION_CONNECTION_NAME' | translate}}</th> |                     </th> | ||||||
|  |                     <th ng-class="{'sort-primary': isSortedBy('activeConnection.startDate'), 'sort-descending': wrapperOrder.descending}" | ||||||
|  |                         ng-click="toggleSort('activeConnection.startDate')"> | ||||||
|  |                         {{'MANAGE_SESSION.TABLE_HEADER_SESSION_STARTDATE' | translate}} | ||||||
|  |                     </th> | ||||||
|  |                     <th ng-class="{'sort-primary': isSortedBy('activeConnection.remoteHost'), 'sort-descending': wrapperOrder.descending}" | ||||||
|  |                         ng-click="toggleSort('activeConnection.remoteHost')"> | ||||||
|  |                         {{'MANAGE_SESSION.TABLE_HEADER_SESSION_REMOTEHOST' | translate}} | ||||||
|  |                     </th> | ||||||
|  |                     <th ng-class="{'sort-primary': isSortedBy('name'), 'sort-descending': wrapperOrder.descending}" | ||||||
|  |                         ng-click="toggleSort('name')"> | ||||||
|  |                         {{'MANAGE_SESSION.TABLE_HEADER_SESSION_CONNECTION_NAME' | translate}} | ||||||
|  |                     </th> | ||||||
|                 </tr> |                 </tr> | ||||||
|             </thead> |             </thead> | ||||||
|             <tbody> |             <tbody> | ||||||
| @@ -55,7 +67,7 @@ THE SOFTWARE. | |||||||
|                     <td>{{wrapper.activeConnection.username}}</td> |                     <td>{{wrapper.activeConnection.username}}</td> | ||||||
|                     <td>{{wrapper.activeConnection.startDate | date:'short'}}</td> |                     <td>{{wrapper.activeConnection.startDate | date:'short'}}</td> | ||||||
|                     <td>{{wrapper.activeConnection.remoteHost}}</td> |                     <td>{{wrapper.activeConnection.remoteHost}}</td> | ||||||
|                     <td>{{connections[wrapper.activeConnection.connectionIdentifier].name}}</td> |                     <td>{{wrapper.name}}</td> | ||||||
|                 </tr> |                 </tr> | ||||||
|             </tbody> |             </tbody> | ||||||
|         </table> |         </table> | ||||||
| @@ -66,7 +78,7 @@ THE SOFTWARE. | |||||||
|         </p> |         </p> | ||||||
|  |  | ||||||
|         <!-- Pager for session list --> |         <!-- Pager for session list --> | ||||||
|         <guac-pager page="wrapperPage" page-size="25" items="wrappers | orderBy : 'username'"></guac-pager> |         <guac-pager page="wrapperPage" page-size="25" items="wrappers | orderBy : wrapperOrder.predicate"></guac-pager> | ||||||
|     </div> |     </div> | ||||||
|  |  | ||||||
| </div> | </div> | ||||||
| @@ -31,10 +31,20 @@ angular.module('manage').factory('ActiveConnectionWrapper', [ | |||||||
|      * properties, such as a checked option. |      * properties, such as a checked option. | ||||||
|      *  |      *  | ||||||
|      * @constructor |      * @constructor | ||||||
|  |      * @param {String} name | ||||||
|  |      *     The display name of the active connection. | ||||||
|  |      * | ||||||
|      * @param {ActiveConnection} activeConnection |      * @param {ActiveConnection} activeConnection | ||||||
|      *     The ActiveConnection to wrap. |      *     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. |          * The wrapped ActiveConnection. | ||||||
|   | |||||||
							
								
								
									
										115
									
								
								guacamole/src/main/webapp/app/manage/types/StableSort.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -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; | ||||||
|  |  | ||||||
|  | }]); | ||||||
| @@ -127,7 +127,7 @@ | |||||||
|     background-repeat: no-repeat; |     background-repeat: no-repeat; | ||||||
|     background-size: 1em; |     background-size: 1em; | ||||||
|     background-position: center center; |     background-position: center center; | ||||||
|     background-image: url('images/action-icons/guac-open-downward.png'); |     background-image: url('images/arrows/down.png'); | ||||||
|  |  | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| Before Width: | Height: | Size: 3.1 KiB | 
| Before Width: | Height: | Size: 2.7 KiB | 
| Before Width: | Height: | Size: 2.7 KiB | 
| Before Width: | Height: | Size: 3.1 KiB | 
| Before Width: | Height: | Size: 282 B After Width: | Height: | Size: 282 B | 
							
								
								
									
										
											BIN
										
									
								
								guacamole/src/main/webapp/images/arrows/up.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 237 B |