diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/tunnel/TunnelRESTService.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/tunnel/TunnelRESTService.java index 4713d1166..1d4c68d25 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/tunnel/TunnelRESTService.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/tunnel/TunnelRESTService.java @@ -25,20 +25,24 @@ package org.glyptodon.guacamole.net.basic.rest.tunnel; import com.google.inject.Inject; import java.util.ArrayList; import java.util.Collection; +import java.util.HashMap; import java.util.List; +import java.util.Map; import javax.ws.rs.Consumes; -import javax.ws.rs.DELETE; import javax.ws.rs.GET; import javax.ws.rs.Path; -import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.MediaType; +import org.glyptodon.guacamole.GuacamoleClientException; import org.glyptodon.guacamole.GuacamoleException; +import org.glyptodon.guacamole.GuacamoleUnsupportedException; import org.glyptodon.guacamole.net.GuacamoleTunnel; import org.glyptodon.guacamole.net.auth.ConnectionRecord; import org.glyptodon.guacamole.net.auth.UserContext; +import org.glyptodon.guacamole.net.basic.rest.APIPatch; import org.glyptodon.guacamole.net.basic.rest.AuthProviderRESTExposure; +import org.glyptodon.guacamole.net.basic.rest.PATCH; import org.glyptodon.guacamole.net.basic.rest.auth.AuthenticationService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -73,7 +77,8 @@ public class TunnelRESTService { * performing the operation. * * @return - * The tunnels of all active connections visible to the current user. + * A map of the tunnels of all active connections visible to the + * current user, where the key of each entry is the tunnel's UUID. * * @throws GuacamoleException * If an error occurs while retrieving the tunnels. @@ -81,19 +86,21 @@ public class TunnelRESTService { @GET @Path("/") @AuthProviderRESTExposure - public List getTunnels(@QueryParam("token") String authToken) + public Map getTunnels(@QueryParam("token") String authToken) throws GuacamoleException { UserContext userContext = authenticationService.getUserContext(authToken); // Retrieve all active tunnels - List apiTunnels = new ArrayList(); + Map apiTunnels = new HashMap(); for (ConnectionRecord record : userContext.getActiveConnections()) { // Locate associated tunnel and UUID GuacamoleTunnel tunnel = record.getTunnel(); - if (tunnel != null) - apiTunnels.add(new APITunnel(record, tunnel.getUUID().toString())); + if (tunnel != null) { + APITunnel apiTunnel = new APITunnel(record, tunnel.getUUID().toString()); + apiTunnels.put(apiTunnel.getUUID(), apiTunnel); + } } @@ -109,24 +116,41 @@ public class TunnelRESTService { * The authentication token that is used to authenticate the user * performing the operation. * - * @param tunnelUUIDs - * The UUIDs associated with the tunnels being deleted. + * @param patches + * The tunnel patches to apply for this request. * * @throws GuacamoleException * If an error occurs while deleting the tunnels. */ - @DELETE + @PATCH @Path("/") @AuthProviderRESTExposure - public void deleteTunnels(@QueryParam("token") String authToken, - @QueryParam("tunnelUUID") Collection tunnelUUIDs) - throws GuacamoleException { + public void patchTunnels(@QueryParam("token") String authToken, + List> patches) throws GuacamoleException { // Attempt to get all requested tunnels UserContext userContext = authenticationService.getUserContext(authToken); - Collection records = userContext.getActiveConnections(tunnelUUIDs); + // Build list of tunnels to delete + Collection tunnelUUIDs = new ArrayList(patches.size()); + for (APIPatch patch : patches) { + + // Only remove is supported + if (patch.getOp() != APIPatch.Operation.remove) + throw new GuacamoleUnsupportedException("Only the \"remove\" operation is supported when patching tunnels."); + + // Retrieve and validate path + String path = patch.getPath(); + if (!path.startsWith("/")) + throw new GuacamoleClientException("Patch paths must start with \"/\"."); + + // Add UUID + tunnelUUIDs.add(path.substring(1)); + + } + // Close each tunnel, if not already closed + Collection records = userContext.getActiveConnections(tunnelUUIDs); for (ConnectionRecord record : records) { GuacamoleTunnel tunnel = record.getTunnel(); diff --git a/guacamole/src/main/webapp/app/manage/controllers/manageSessionsController.js b/guacamole/src/main/webapp/app/manage/controllers/manageSessionsController.js index 8aa49022f..3ae1e77e8 100644 --- a/guacamole/src/main/webapp/app/manage/controllers/manageSessionsController.js +++ b/guacamole/src/main/webapp/app/manage/controllers/manageSessionsController.js @@ -74,11 +74,11 @@ angular.module('manage').controller('manageSessionsController', ['$scope', '$inj $scope.connections = {}; /** - * The count of currently selected tunnel wrappers. + * Map of all currently-selected tunnel wrappers by UUID. * - * @type Number + * @type Object. */ - var selectedWrapperCount = 0; + var selectedWrappers = {}; /** * Adds the given connection to the internal set of visible @@ -126,9 +126,9 @@ angular.module('manage').controller('manageSessionsController', ['$scope', '$inj // Wrap all active tunnels for sake of display $scope.wrappers = []; - tunnels.forEach(function wrapActiveTunnel(tunnel) { - $scope.wrappers.push(new ActiveTunnelWrapper(tunnel)); - }); + for (var tunnelUUID in tunnels) { + $scope.wrappers.push(new ActiveTunnelWrapper(tunnels[tunnelUUID])); + } }); @@ -147,12 +147,24 @@ angular.module('manage').controller('manageSessionsController', ['$scope', '$inj }; + /** + * An action to be provided along with the object sent to showStatus which + * closes the currently-shown status dialog. + */ + var ACKNOWLEDGE_ACTION = { + name : "MANAGE_SESSION.ACTION_ACKNOWLEDGE", + // Handle action + callback : function acknowledgeCallback() { + guacNotification.showStatus(false); + } + }; + /** * An action to be provided along with the object sent to showStatus which * closes the currently-shown status dialog. */ var CANCEL_ACTION = { - name : "MANAGE_USER.ACTION_CANCEL", + name : "MANAGE_SESSION.ACTION_CANCEL", // Handle action callback : function cancelCallback() { guacNotification.showStatus(false); @@ -178,7 +190,31 @@ angular.module('manage').controller('manageSessionsController', ['$scope', '$inj * confirmation. */ var deleteSessionsImmediately = function deleteSessionsImmediately() { - // TODO: Use a batch delete function to delete the sessions. + + // Perform deletion + tunnelService.deleteActiveTunnels(Object.keys(selectedWrappers)) + .success(function tunnelsDeleted() { + + // Remove deleted tunnels from wrapper array + $scope.wrappers = $scope.wrappers.filter(function tunnelStillExists(wrapper) { + return !(wrapper.tunnel.uuid in selectedWrappers); + }); + + // Clear selection + selectedWrappers = {}; + + }) + + // Notify of any errors + .error(function tunnelDeletionFailed(error) { + guacNotification.showStatus({ + 'className' : 'error', + 'title' : 'MANAGE_SESSION.DIALOG_HEADER_ERROR', + 'text' : error.message, + 'actions' : [ ACKNOWLEDGE_ACTION ] + }); + }); + }; /** @@ -201,20 +237,31 @@ angular.module('manage').controller('manageSessionsController', ['$scope', '$inj * true if selected sessions can be deleted, false otherwise. */ $scope.canDeleteSessions = function canDeleteSessions() { - return selectedWrapperCount > 0; + + // We can delete sessions if at least one is selected + for (var tunnelUUID in selectedWrappers) + return true; + + return false; + }; /** * Called whenever a tunnel wrapper changes selected status. * - * @param {Boolean} selected - * Whether the wrapper is now selected. + * @param {ActiveTunnelWrapper} wrapper + * The wrapper whose selected status has changed. */ - $scope.wrapperSelectionChange = function wrapperSelectionChange(selected) { - if (selected) - selectedWrapperCount++; + $scope.wrapperSelectionChange = function wrapperSelectionChange(wrapper) { + + // Add wrapper to map if selected + if (wrapper.checked) + selectedWrappers[wrapper.tunnel.uuid] = wrapper; + + // Otherwise, remove wrapper from map else - selectedWrapperCount--; + delete selectedWrappers[wrapper.tunnel.uuid]; + }; }]); diff --git a/guacamole/src/main/webapp/app/manage/templates/manageSessions.html b/guacamole/src/main/webapp/app/manage/templates/manageSessions.html index 7bb34cf5d..98c86ab19 100644 --- a/guacamole/src/main/webapp/app/manage/templates/manageSessions.html +++ b/guacamole/src/main/webapp/app/manage/templates/manageSessions.html @@ -56,7 +56,7 @@ THE SOFTWARE. - + {{wrapper.tunnel.username}} diff --git a/guacamole/src/main/webapp/app/rest/services/tunnelService.js b/guacamole/src/main/webapp/app/rest/services/tunnelService.js index ca9c5a838..8ce18d867 100644 --- a/guacamole/src/main/webapp/app/rest/services/tunnelService.js +++ b/guacamole/src/main/webapp/app/rest/services/tunnelService.js @@ -30,12 +30,12 @@ angular.module('rest').factory('tunnelService', ['$http', 'authenticationService /** * Makes a request to the REST API to get the list of active tunnels, - * returning a promise that provides an array of @link{ActiveTunnel} + * returning a promise that provides a map of @link{ActiveTunnel} * objects if successful. * - * @returns {Promise.} - * A promise which will resolve with an array of @link{ActiveTunnel} - * objects upon success. + * @returns {Promise.>} + * A promise which will resolve with a map of @link{ActiveTunnel} + * objects, where each key is the UUID of the corresponding tunnel. */ service.getActiveTunnels = function getActiveTunnels() { @@ -54,31 +54,41 @@ angular.module('rest').factory('tunnelService', ['$http', 'authenticationService }; /** - * Makes a request to the REST API to delete the tunnel having the given - * UUID, effectively disconnecting the tunnel, returning a promise that can - * be used for processing the results of the call. + * Makes a request to the REST API to delete the tunnels having the given + * UUIDs, effectively disconnecting the tunnels, returning a promise that + * can be used for processing the results of the call. * - * @param {String} uuid - * The UUID of the tunnel to delete. + * @param {String[]} uuids + * The UUIDs of the tunnels to delete. * * @returns {Promise} * A promise for the HTTP call which will succeed if and only if the * delete operation is successful. */ - service.deleteActiveTunnel = function deleteActiveTunnel(uuid) { + service.deleteActiveTunnels = function deleteActiveTunnels(uuids) { // Build HTTP parameters set var httpParameters = { token : authenticationService.getCurrentToken() }; - // Delete connection - return $http({ - method : 'DELETE', - url : 'api/tunnels/' + encodeURIComponent(uuid), - params : httpParameters + // Convert provided array of UUIDs to a patch + var tunnelPatch = []; + uuids.forEach(function addTunnelPatch(uuid) { + tunnelPatch.push({ + op : 'remove', + path : '/' + uuid + }); }); + // Perform tunnel deletion via PATCH + return $http({ + method : 'PATCH', + url : 'api/tunnels', + params : httpParameters, + data : tunnelPatch + }); + }; return service; diff --git a/guacamole/src/main/webapp/translations/en_US.json b/guacamole/src/main/webapp/translations/en_US.json index e19e4dfe1..b8e5f8039 100644 --- a/guacamole/src/main/webapp/translations/en_US.json +++ b/guacamole/src/main/webapp/translations/en_US.json @@ -236,7 +236,9 @@ "MANAGE_SESSION" : { - "ACTION_DELETE" : "Kill all selected sessions", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", + "ACTION_DELETE" : "Kill all selected sessions", "DIALOG_HEADER_CONFIRM_DELETE" : "Kill Sessions", "DIALOG_HEADER_ERROR" : "Error",