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