From aae80292cb77ed298306c4fcdd646ff22b3ba603 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Wed, 16 Jun 2021 01:57:24 -0700 Subject: [PATCH] GUACAMOLE-724: Abstract away groups of running clients within their own type. --- .../client/controllers/clientController.js | 97 ++++---- .../app/client/directives/guacClientPanel.js | 94 ++++---- .../app/client/directives/guacTiledClients.js | 73 ++---- .../client/directives/guacTiledThumbnails.js | 73 ++++++ .../app/client/services/guacClientManager.js | 168 +++++++++++++- .../src/app/client/templates/client.html | 11 +- .../app/client/templates/guacClientPanel.html | 18 +- .../client/templates/guacTiledClients.html | 4 +- .../client/templates/guacTiledThumbnails.html | 7 + .../src/app/client/types/ManagedClient.js | 5 +- .../app/client/types/ManagedClientGroup.js | 209 ++++++++++++++++++ .../home/directives/guacRecentConnections.js | 41 +--- .../home/templates/guacRecentConnections.html | 17 -- .../app/index/controllers/indexController.js | 19 +- .../styles/other-connections.css | 2 +- guacamole/src/main/frontend/src/index.html | 5 + 16 files changed, 603 insertions(+), 240 deletions(-) create mode 100644 guacamole/src/main/frontend/src/app/client/directives/guacTiledThumbnails.js create mode 100644 guacamole/src/main/frontend/src/app/client/templates/guacTiledThumbnails.html create mode 100644 guacamole/src/main/frontend/src/app/client/types/ManagedClientGroup.js rename guacamole/src/main/frontend/src/app/{client => index}/styles/other-connections.css (99%) diff --git a/guacamole/src/main/frontend/src/app/client/controllers/clientController.js b/guacamole/src/main/frontend/src/app/client/controllers/clientController.js index e107cef5b..ca225ab90 100644 --- a/guacamole/src/main/frontend/src/app/client/controllers/clientController.js +++ b/guacamole/src/main/frontend/src/app/client/controllers/clientController.js @@ -26,6 +26,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams // Required types var ConnectionGroup = $injector.get('ConnectionGroup'); var ManagedClient = $injector.get('ManagedClient'); + var ManagedClientGroup = $injector.get('ManagedClientGroup'); var ManagedClientState = $injector.get('ManagedClientState'); var ManagedFilesystem = $injector.get('ManagedFilesystem'); var Protocol = $injector.get('Protocol'); @@ -152,37 +153,66 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams }; /** - * The client which should be attached to the client UI. + * The set of clients that should be attached to the client UI. This will + * be immediately initialized by a call to updateAttachedClients() below. * - * @type ManagedClient[] + * @type ManagedClientGroup[] */ - $scope.clients = []; + $scope.clientGroup = null; /** - * All active clients which are not any current client ($scope.clients). - * Each key is the ID of the connection used by that client. - * - * @type Object. + * @borrows ManagedClientGroup.getName */ - $scope.otherClients = {}; + $scope.getName = ManagedClientGroup.getName; /** - * Reloads the contents of $scope.clients and $scope.otherClients to - * reflect the client IDs currently listed in the URL. + * Reloads the contents of $scope.clientGroup to reflect the client IDs + * currently listed in the URL. */ var updateAttachedClients = function updateAttachedClients() { - var ids = $routeParams.id.split(/[ +]/); + var previousClients = $scope.clientGroup ? $scope.clientGroup.clients.slice() : []; + detachCurrentGroup(); - $scope.clients = []; - $scope.otherClients = angular.extend({}, guacClientManager.getManagedClients()); + $scope.clientGroup = guacClientManager.getManagedClientGroup($routeParams.id); + $scope.clientGroup.attached = true; - // Separate active clients by whether they should be displayed within - // the current view - ids.forEach(function groupClients(id) { - $scope.clients.push(guacClientManager.getManagedClient(id)); - delete $scope.otherClients[id]; - }); + // Ensure menu is closed if updated view is not a modification of the + // current view (has no clients in common). The menu should remain open + // only while the current view is being modified, not when navigating + // to an entirely different view. + if (_.isEmpty(_.intersection(previousClients, $scope.clientGroup.clients))) + $scope.menu.shown = false; + + }; + + /** + * Detaches the ManagedClientGroup currently attached to the client + * interface via $scope.clientGroup such that the interface can be safely + * cleaned up or another ManagedClientGroup can take its place. + */ + var detachCurrentGroup = function detachCurrentGroup() { + + var managedClientGroup = $scope.clientGroup; + if (managedClientGroup) { + + // Flag group as detached + managedClientGroup.attached = false; + + // Remove all disconnected clients from management (the user has + // seen their status) + _.filter(managedClientGroup.clients, client => { + + var connectionState = client.clientState.connectionState; + return connectionState === ManagedClientState.ConnectionState.DISCONNECTED + || connectionState === ManagedClientState.ConnectionState.TUNNEL_ERROR + || connectionState === ManagedClientState.ConnectionState.CLIENT_ERROR; + + }).forEach(client => { + guacClientManager.removeManagedClient(client.id); + }); + + } }; @@ -424,7 +454,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams $scope.menu.connectionParameters = ManagedClient.getArgumentModel($scope.client); // Disable client keyboard if the menu is shown - angular.forEach($scope.clients, function updateKeyboardEnabled(client) { + angular.forEach($scope.clientGroup.clients, function updateKeyboardEnabled(client) { client.clientProperties.keyboardEnabled = !menuShown; }); @@ -606,15 +636,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams * otherwise. */ $scope.isConnectionUnstable = function isConnectionUnstable() { - - var unstable = false; - - angular.forEach($scope.clients, function checkStability(client) { - unstable |= client.clientState.tunnelUnstable; - }); - - return unstable; - + return _.findIndex($scope.clientGroup.clients, client => client.clientState.tunnelUnstable) !== -1; }; @@ -830,22 +852,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams // Clean up when view destroyed $scope.$on('$destroy', function clientViewDestroyed() { - - // Remove client from client manager if no longer connected - var managedClient = $scope.client; - if (managedClient) { - - // Get current connection state - var connectionState = managedClient.clientState.connectionState; - - // If disconnected, remove from management - if (connectionState === ManagedClientState.ConnectionState.DISCONNECTED - || connectionState === ManagedClientState.ConnectionState.TUNNEL_ERROR - || connectionState === ManagedClientState.ConnectionState.CLIENT_ERROR) - guacClientManager.removeManagedClient(managedClient.id); - - } - + detachCurrentGroup(); }); }]); diff --git a/guacamole/src/main/frontend/src/app/client/directives/guacClientPanel.js b/guacamole/src/main/frontend/src/app/client/directives/guacClientPanel.js index 658be2fdd..23feccd72 100644 --- a/guacamole/src/main/frontend/src/app/client/directives/guacClientPanel.js +++ b/guacamole/src/main/frontend/src/app/client/directives/guacClientPanel.js @@ -29,6 +29,7 @@ angular.module('client').directive('guacClientPanel', ['$injector', function gua var sessionStorageFactory = $injector.get('sessionStorageFactory'); // Required types + var ManagedClientGroup = $injector.get('ManagedClientGroup'); var ManagedClientState = $injector.get('ManagedClientState'); /** @@ -49,12 +50,12 @@ angular.module('client').directive('guacClientPanel', ['$injector', function gua scope: { /** - * The ManagedClient instances associated with the active + * The ManagedClientGroup instances associated with the active * connections to be displayed within this panel. * - * @type ManagedClient[]|Object. + * @type ManagedClientGroup[] */ - clients : '=' + clientGroups : '=' }, templateUrl: 'app/client/templates/guacClientPanel.html', @@ -75,71 +76,68 @@ angular.module('client').directive('guacClientPanel', ['$injector', function gua $scope.panelHidden = panelHidden; /** - * Returns whether this panel currently has any clients associated - * with it. + * Returns whether this panel currently has any client groups + * associated with it. * * @return {Boolean} - * true if at least one client is associated with this panel, - * false otherwise. + * true if at least one client group is associated with this + * panel, false otherwise. */ - $scope.hasClients = function hasClients() { - return !!_.find($scope.clients, $scope.isManaged); + $scope.hasClientGroups = function hasClientGroups() { + return $scope.clientGroups && $scope.clientGroups.length; }; /** - * Returns whether the status of the given client has changed in a - * way that requires the user's attention. This may be due to an - * error, or due to a server-initiated disconnect. + * @borrows ManagedClientGroup.getIdentifier + */ + $scope.getIdentifier = ManagedClientGroup.getIdentifier; + + /** + * @borrows ManagedClientGroup.getTitle + */ + $scope.getTitle = ManagedClientGroup.getTitle; + + /** + * Returns whether the status of any client within the given client + * group has changed in a way that requires the user's attention. + * This may be due to an error, or due to a server-initiated + * disconnect. * - * @param {ManagedClient} client - * The client to test. + * @param {ManagedClientGroup} clientGroup + * The client group to test. * * @returns {Boolean} * true if the given client requires the user's attention, * false otherwise. */ - $scope.hasStatusUpdate = function hasStatusUpdate(client) { + $scope.hasStatusUpdate = function hasStatusUpdate(clientGroup) { + return _.findIndex(clientGroup.clients, (client) => { - // Test whether the client has encountered an error - switch (client.clientState.connectionState) { - case ManagedClientState.ConnectionState.CONNECTION_ERROR: - case ManagedClientState.ConnectionState.TUNNEL_ERROR: - case ManagedClientState.ConnectionState.DISCONNECTED: - return true; - } + // Test whether the client has encountered an error + switch (client.clientState.connectionState) { + case ManagedClientState.ConnectionState.CONNECTION_ERROR: + case ManagedClientState.ConnectionState.TUNNEL_ERROR: + case ManagedClientState.ConnectionState.DISCONNECTED: + return true; + } - return false; + return false; + }) !== -1; }; /** - * Returns whether the given client is currently being managed by - * the guacClientManager service. + * Initiates an orderly disconnect of all clients within the given + * group. The clients are removed from management such that + * attempting to connect to any of the same connections will result + * in new connections being established, rather than displaying a + * notification that the connection has ended. * - * @param {ManagedClient} client - * The client to test. - * - * @returns {Boolean} - * true if the given client is being managed by the - * guacClientManager service, false otherwise. + * @param {ManagedClientGroup} clientGroup + * The group of clients to disconnect. */ - $scope.isManaged = function isManaged(client) { - return !!guacClientManager.getManagedClients()[client.id]; - }; - - /** - * Initiates an orderly disconnect of the given client. The client - * is removed from management such that attempting to connect to - * the same connection will result in a new connection being - * established, rather than displaying a notification that the - * connection has ended. - * - * @param {type} client - * @returns {undefined} - */ - $scope.disconnect = function disconnect(client) { - client.client.disconnect(); - guacClientManager.removeManagedClient(client.id); + $scope.disconnect = function disconnect(clientGroup) { + guacClientManager.removeManagedClientGroup(ManagedClientGroup.getIdentifier(clientGroup)); }; /** diff --git a/guacamole/src/main/frontend/src/app/client/directives/guacTiledClients.js b/guacamole/src/main/frontend/src/app/client/directives/guacTiledClients.js index 83f8b25e6..b70cd83a8 100644 --- a/guacamole/src/main/frontend/src/app/client/directives/guacTiledClients.js +++ b/guacamole/src/main/frontend/src/app/client/directives/guacTiledClients.js @@ -32,50 +32,17 @@ angular.module('client').directive('guacTiledClients', [function guacTiledClient directive.scope = { /** - * The Guacamole clients that should be displayed in an evenly-tiled - * grid arrangement. + * The group of Guacamole clients that should be displayed in an + * evenly-tiled grid arrangement. * - * @type ManagedClient[] + * @type ManagedClientGroup */ - clients : '=' + clientGroup : '=' }; directive.controller = ['$scope', '$injector', '$element', - function guacTiledListController($scope, $injector, $element) { - - /** - * Returns the number of columns that should be used to evenly arrange - * all provided clients in a tiled grid. - * - * @returns {Number} - * The number of columns that should be used for the grid of - * clients. - */ - var getColumns = function getColumns() { - - if (!$scope.clients || !$scope.clients.length) - return 0; - - return Math.ceil(Math.sqrt($scope.clients.length)); - - }; - - /** - * Returns the number of rows that should be used to evenly arrange all - * provided clients in a tiled grid. - * - * @returns {Number} - * The number of rows that should be used for the grid of clients. - */ - var getRows = function getRows() { - - if (!$scope.clients || !$scope.clients.length) - return 0; - - return Math.ceil($scope.clients.length / getColumns()); - - }; + function guacTiledClientsController($scope, $injector, $element) { /** * Assigns keyboard focus to the given client, allowing that client to @@ -86,7 +53,14 @@ angular.module('client').directive('guacTiledClients', [function guacTiledClient * The client that should receive keyboard focus. */ $scope.assignFocus = function assignFocus(client) { + + // Clear focus of all other clients + $scope.clientGroup.clients.forEach(client => { + client.clientProperties.focused = false; + }); + client.clientProperties.focused = true; + }; /** @@ -98,10 +72,10 @@ angular.module('client').directive('guacTiledClients', [function guacTiledClient * otherwise. */ $scope.hasMultipleClients = function hasMultipleClients() { - return $scope.clients && $scope.clients.length > 1; + return $scope.clientGroup && $scope.clientGroup.clients.length > 1; }; - /** + /** * Returns the CSS width that should be applied to each tile to * achieve an even arrangement. * @@ -109,7 +83,7 @@ angular.module('client').directive('guacTiledClients', [function guacTiledClient * The CSS width that should be applied to each tile. */ $scope.getTileWidth = function getTileWidth() { - return Math.floor(100 / getColumns()) + '%'; + return Math.floor(100 / $scope.clientGroup.columns) + '%'; }; /** @@ -120,22 +94,7 @@ angular.module('client').directive('guacTiledClients', [function guacTiledClient * The CSS height that should be applied to each tile. */ $scope.getTileHeight = function getTileHeight() { - return Math.floor(100 / getRows()) + '%'; - }; - - /** - * Returns the display title of the given Guacamole client. If the - * title is not yet known, a placeholder title will be returned. - * - * @param {ManagedClient} client - * The client whose title should be retrieved. - * - * @returns {String} - * The title of the given client, or a placeholder title if the - * client's title is not yet known. - */ - $scope.getClientTitle = function getClientTitle(client) { - return client.title || '...'; + return Math.floor(100 / $scope.clientGroup.rows) + '%'; }; }]; diff --git a/guacamole/src/main/frontend/src/app/client/directives/guacTiledThumbnails.js b/guacamole/src/main/frontend/src/app/client/directives/guacTiledThumbnails.js new file mode 100644 index 000000000..0c3916c48 --- /dev/null +++ b/guacamole/src/main/frontend/src/app/client/directives/guacTiledThumbnails.js @@ -0,0 +1,73 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * A directive for displaying a group of Guacamole clients as a non-interactive + * thumbnail of tiled client displays. + */ +angular.module('client').directive('guacTiledThumbnails', [function guacTiledThumbnails() { + + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'app/client/templates/guacTiledThumbnails.html' + }; + + directive.scope = { + + /** + * The group of clients to display as a thumbnail of tiled client + * displays. + * + * @type ManagedClientGroup + */ + clientGroup : '=' + + }; + + directive.controller = ['$scope', '$injector', '$element', + function guacTiledThumbnailsController($scope, $injector, $element) { + + /** + * Returns the CSS width that should be applied to each tile to + * achieve an even arrangement. + * + * @returns {String} + * The CSS width that should be applied to each tile. + */ + $scope.getTileWidth = function getTileWidth() { + return Math.floor(100 / $scope.clientGroup.columns) + '%'; + }; + + /** + * Returns the CSS height that should be applied to each tile to + * achieve an even arrangement. + * + * @returns {String} + * The CSS height that should be applied to each tile. + */ + $scope.getTileHeight = function getTileHeight() { + return Math.floor(100 / $scope.clientGroup.rows) + '%'; + }; + + }]; + + return directive; + +}]); \ No newline at end of file diff --git a/guacamole/src/main/frontend/src/app/client/services/guacClientManager.js b/guacamole/src/main/frontend/src/app/client/services/guacClientManager.js index 71918ea67..5c70fb3dc 100644 --- a/guacamole/src/main/frontend/src/app/client/services/guacClientManager.js +++ b/guacamole/src/main/frontend/src/app/client/services/guacClientManager.js @@ -24,7 +24,8 @@ angular.module('client').factory('guacClientManager', ['$injector', function guacClientManager($injector) { // Required types - var ManagedClient = $injector.get('ManagedClient'); + var ManagedClient = $injector.get('ManagedClient'); + var ManagedClientGroup = $injector.get('ManagedClientGroup'); // Required services var $window = $injector.get('$window'); @@ -56,6 +57,53 @@ angular.module('client').factory('guacClientManager', ['$injector', return storedManagedClients(); }; + /** + * Getter/setter which retrieves or sets the array of all active managed + * client groups. + * + * @type Function + */ + var storedManagedClientGroups = sessionStorageFactory.create([], function destroyClientGroupStorage() { + + // Disconnect all clients when storage is destroyed + service.clear(); + + }); + + /** + * Returns an array of all managed client groups. + * + * @returns {ManagedClientGroup[]>} + * An array of all active managed client groups. + */ + service.getManagedClientGroups = function getManagedClientGroups() { + return storedManagedClientGroups(); + }; + + /** + * Removes the ManagedClient with the given ID from all + * ManagedClientGroups, automatically adjusting the tile size of the + * clients that remain in each group. All client groups that are empty + * after the client is removed will also be removed. + * + * @param {string} id + * The ID of the ManagedClient to remove. + */ + var ungroupManagedClient = function ungroupManagedClient(id) { + + var managedClientGroups = storedManagedClientGroups(); + + // Remove client from all groups + managedClientGroups.forEach(group => { + _.remove(group.clients, client => (client.id === id)); + ManagedClientGroup.recalculateTiles(group); + }); + + // Remove any groups that are now empty + _.remove(managedClientGroups, group => !group.clients.length); + + }; + /** * Removes the existing ManagedClient associated with the connection having * the given ID, if any. If no such a ManagedClient already exists, this @@ -67,13 +115,16 @@ angular.module('client').factory('guacClientManager', ['$injector', * @returns {Boolean} * true if an existing client was removed, false otherwise. */ - service.removeManagedClient = function replaceManagedClient(id) { - + service.removeManagedClient = function removeManagedClient(id) { + var managedClients = storedManagedClients(); // Remove client if it exists if (id in managedClients) { + // Pull client out of any containing groups + ungroupManagedClient(id); + // Disconnect and remove managedClients[id].client.disconnect(); delete managedClients[id]; @@ -102,11 +153,31 @@ angular.module('client').factory('guacClientManager', ['$injector', */ service.replaceManagedClient = function replaceManagedClient(id) { - // Disconnect any existing client - service.removeManagedClient(id); + var managedClients = storedManagedClients(); + var managedClientGroups = storedManagedClientGroups(); - // Set new client - return storedManagedClients()[id] = ManagedClient.getInstance(id); + // Remove client if it exists + if (id in managedClients) { + + var hadFocus = managedClients[id].clientProperties.focused; + managedClients[id].client.disconnect(); + delete managedClients[id]; + + // Remove client from all groups + managedClientGroups.forEach(group => { + + var index = _.findIndex(group.clients, client => (client.id === id)); + if (index === -1) + return; + + group.clients[index] = managedClients[id] = ManagedClient.getInstance(id); + managedClients[id].clientProperties.focused = hadFocus; + + }); + + } + + return managedClients[id]; }; @@ -126,6 +197,10 @@ angular.module('client').factory('guacClientManager', ['$injector', var managedClients = storedManagedClients(); + // Ensure any existing client is removed from its containing group + // prior to being returned + ungroupManagedClient(id); + // Create new managed client if it doesn't already exist if (!(id in managedClients)) managedClients[id] = ManagedClient.getInstance(id); @@ -136,7 +211,81 @@ angular.module('client').factory('guacClientManager', ['$injector', }; /** - * Disconnects and removes all currently-connected clients. + * Returns the ManagedClientGroup having the given ID. If no such + * ManagedClientGroup exists, a new ManagedClientGroup is created by + * extracting the relevant connections from the ID. + * + * @param {String} id + * The ID of the ManagedClientGroup to retrieve or create. + * + * @returns {ManagedClientGroup} + * The ManagedClientGroup having the given ID. + */ + service.getManagedClientGroup = function getManagedClientGroup(id) { + + var clients = []; + var clientIds = ManagedClientGroup.getClientIdentifiers(id); + + // Separate active clients by whether they should be displayed within + // the current view + clientIds.forEach(function groupClients(id) { + clients.push(service.getManagedClient(id)); + }); + + if (clients.length === 1) { + clients[0].clientProperties.focused = true; + } + + var group = new ManagedClientGroup({ + clients : clients + }); + + var managedClientGroups = storedManagedClientGroups(); + managedClientGroups.push(group); + return group; + + }; + + /** + * Removes the existing ManagedClientGroup having the given ID, if any, + * disconnecting and removing all ManagedClients associated with that + * group. If no such a ManagedClientGroup currently exists, this function + * has no effect. + * + * @param {String} id + * The ID of the ManagedClientGroup to remove. + * + * @returns {Boolean} + * true if a ManagedClientGroup was removed, false otherwise. + */ + service.removeManagedClientGroup = function removeManagedClientGroup(id) { + + var managedClients = storedManagedClients(); + var managedClientGroups = storedManagedClientGroups(); + + // Remove all matching groups (there SHOULD only be one) + var removed = _.remove(managedClientGroups, (group) => ManagedClientGroup.getIdentifier(group) === id); + + // Disconnect all clients associated with the removed group(s) + removed.forEach((group) => { + group.clients.forEach((client) => { + + var id = client.id; + if (managedClients[id]) { + managedClients[id].client.disconnect(); + delete managedClients[id]; + } + + }); + }); + + return !!removed.length; + + }; + + /** + * Disconnects and removes all currently-connected clients and client + * groups. */ service.clear = function clear() { @@ -146,8 +295,9 @@ angular.module('client').factory('guacClientManager', ['$injector', for (var id in managedClients) managedClients[id].client.disconnect(); - // Clear managed clients + // Clear managed clients and client groups storedManagedClients({}); + storedManagedClientGroups([]); }; diff --git a/guacamole/src/main/frontend/src/app/client/templates/client.html b/guacamole/src/main/frontend/src/app/client/templates/client.html index 17f0c502f..4694eaff1 100644 --- a/guacamole/src/main/frontend/src/app/client/templates/client.html +++ b/guacamole/src/main/frontend/src/app/client/templates/client.html @@ -9,12 +9,7 @@
- - - -
- -
+
@@ -52,9 +47,9 @@
-

{{client.name}}

+

{{ getName(clientGroup) }}

- +
+ ng-class="{ 'has-clients': hasClientGroups(), 'hidden' : panelHidden() }">
    -
  • - - -
    - -
    -
    {{ client.value.title }}
    +
    + +
    {{ getTitle(clientGroup) }}
  • diff --git a/guacamole/src/main/frontend/src/app/client/templates/guacTiledClients.html b/guacamole/src/main/frontend/src/app/client/templates/guacTiledClients.html index ecae4efa3..18d9278e9 100644 --- a/guacamole/src/main/frontend/src/app/client/templates/guacTiledClients.html +++ b/guacamole/src/main/frontend/src/app/client/templates/guacTiledClients.html @@ -1,12 +1,12 @@
    • -

      {{ getClientTitle(client) }}

      +

      {{ client.title }}

      diff --git a/guacamole/src/main/frontend/src/app/client/templates/guacTiledThumbnails.html b/guacamole/src/main/frontend/src/app/client/templates/guacTiledThumbnails.html new file mode 100644 index 000000000..e1b0ef336 --- /dev/null +++ b/guacamole/src/main/frontend/src/app/client/templates/guacTiledThumbnails.html @@ -0,0 +1,7 @@ +
        +
      • + +
      • +
      \ No newline at end of file diff --git a/guacamole/src/main/frontend/src/app/client/types/ManagedClient.js b/guacamole/src/main/frontend/src/app/client/types/ManagedClient.js index 4e59b6ae5..3d21b24c3 100644 --- a/guacamole/src/main/frontend/src/app/client/types/ManagedClient.js +++ b/guacamole/src/main/frontend/src/app/client/types/ManagedClient.js @@ -62,8 +62,9 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector', /** * Object which serves as a surrogate interface, encapsulating a Guacamole - * client while it is active, allowing it to be detached and reattached - * from different client views. + * client while it is active, allowing it to be maintained in the + * background. One or more ManagedClients are grouped within + * ManagedClientGroups before being attached to the client view. * * @constructor * @param {ManagedClient|Object} [template={}] diff --git a/guacamole/src/main/frontend/src/app/client/types/ManagedClientGroup.js b/guacamole/src/main/frontend/src/app/client/types/ManagedClientGroup.js new file mode 100644 index 000000000..990591e9e --- /dev/null +++ b/guacamole/src/main/frontend/src/app/client/types/ManagedClientGroup.js @@ -0,0 +1,209 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * Provides the ManagedClientGroup class used by the guacClientManager service. + */ +angular.module('client').factory('ManagedClientGroup', [function defineManagedClientGroup() { + + /** + * Object which serves as a grouping of ManagedClients. Each + * ManagedClientGroup may be attached, detached, and reattached dynamically + * from different client views, with its contents automatically displayed + * in a tiled arrangment if needed. + * + * @constructor + * @param {ManagedClientGroup|Object} [template={}] + * The object whose properties should be copied within the new + * ManagedClientGroup. + */ + var ManagedClientGroup = function ManagedClientGroup(template) { + + // Use empty object by default + template = template || {}; + + /** + * Whether this ManagedClientGroup is currently attached to the client + * interface (true) or is running in the background (false). + * + * @type {boolean} + * @default false + */ + this.attached = template.attached || false; + + /** + * The clients that should be displayed within the client interface + * when this group is attached. + * + * @type {ManagedClient[]} + * @default [] + */ + this.clients = template.clients || []; + + /** + * The number of rows that should be used when arranging the clients + * within this group in a grid. By default, this value is automatically + * calculated from the number of clients. + * + * @type {number} + */ + this.rows = template.rows || ManagedClientGroup.getRows(this); + + /** + * The number of columns that should be used when arranging the clients + * within this group in a grid. By default, this value is automatically + * calculated from the number of clients. + * + * @type {number} + */ + this.columns = template.columns || ManagedClientGroup.getColumns(this); + + }; + + /** + * Updates the number of rows and columns stored within the given + * ManagedClientGroup such that the clients within the group are evenly + * distributed. This function should be called whenever the size of a + * group changes. + * + * @param {ManagedClientGroup} group + * The ManagedClientGroup that should be updated. + */ + ManagedClientGroup.recalculateTiles = function recalculateTiles(group) { + + var recalculated = new ManagedClientGroup({ + clients : group.clients + }); + + group.rows = recalculated.rows; + group.columns = recalculated.columns; + + }; + + /** + * Returns the unique ID representing the given ManagedClientGroup. The ID + * of each ManagedClientGroup consists simply of the IDs of all its + * ManagedClients, separated by periods. + * + * @param {ManagedClientGroup} group + * The ManagedClientGroup to determine the ID of. + * + * @returns {string} + * The unique ID representing the given ManagedClientGroup. + */ + ManagedClientGroup.getIdentifier = function getIdentifier(group) { + return _.map(group.clients, client => client.id).join('.'); + }; + + /** + * Returns an array of client identifiers for all clients contained within + * the given ManagedClientGroup. Order of the identifiers is preserved + * with respect to the order of the clients within the group. + * + * @param {ManagedClientGroup|string} group + * The ManagedClientGroup to retrieve the client identifiers from, + * or its ID. + * + * @returns {string[]} + * The client identifiers of all clients contained within the given + * ManagedClientGroup. + */ + ManagedClientGroup.getClientIdentifiers = function getClientIdentifiers(group) { + + if (_.isString(group)) + return group.split(/\./); + + return group.clients.map(client => client.id); + + }; + + /** + * Returns the number of columns that should be used to evenly arrange + * all provided clients in a tiled grid. + * + * @returns {Number} + * The number of columns that should be used for the grid of + * clients. + */ + ManagedClientGroup.getColumns = function getColumns(group) { + + if (!group.clients.length) + return 0; + + return Math.ceil(Math.sqrt(group.clients.length)); + + }; + + /** + * Returns the number of rows that should be used to evenly arrange all + * provided clients in a tiled grid. + * + * @returns {Number} + * The number of rows that should be used for the grid of clients. + */ + ManagedClientGroup.getRows = function getRows(group) { + + if (!group.clients.length) + return 0; + + return Math.ceil(group.clients.length / ManagedClientGroup.getColumns(group)); + + }; + + /** + * Returns the title which should be displayed as the page title if the + * given client group is attached to the interface. + * + * @param {ManagedClientGroup} group + * The ManagedClientGroup to determine the title of. + * + * @returns {string} + * The title of the given ManagedClientGroup. + */ + ManagedClientGroup.getTitle = function getTitle(group) { + + // Use client-specific title if only one client + if (group.clients.length === 1) + return group.clients[0].title; + + // With multiple clients, somehow combining multiple page titles would + // be confusing. Instead, use the combined names. + return ManagedClientGroup.getName(group); + + }; + + /** + * Returns the combined names of all clients within the given + * ManagedClientGroup, as determined by the names of the associated + * connections or connection groups. + * + * @param {ManagedClientGroup} group + * The ManagedClientGroup to determine the name of. + * + * @returns {string} + * The combined names of all clients within the given + * ManagedClientGroup. + */ + ManagedClientGroup.getName = function getName(group) { + return _.filter(group.clients, (client => !!client.name)).map(client => client.name).join(', ') || '...'; + }; + + return ManagedClientGroup; + +}]); \ No newline at end of file diff --git a/guacamole/src/main/frontend/src/app/home/directives/guacRecentConnections.js b/guacamole/src/main/frontend/src/app/home/directives/guacRecentConnections.js index 13f65b475..c59ad4734 100644 --- a/guacamole/src/main/frontend/src/app/home/directives/guacRecentConnections.js +++ b/guacamole/src/main/frontend/src/app/home/directives/guacRecentConnections.js @@ -18,7 +18,8 @@ */ /** - * A directive which displays the contents of a connection group. + * A directive which displays the recently-accessed connections nested beneath + * each of the given connection groups. */ angular.module('home').directive('guacRecentConnections', [function guacRecentConnections() { @@ -44,21 +45,12 @@ angular.module('home').directive('guacRecentConnections', [function guacRecentCo controller: ['$scope', '$injector', function guacRecentConnectionsController($scope, $injector) { // Required types - var ActiveConnection = $injector.get('ActiveConnection'); var ClientIdentifier = $injector.get('ClientIdentifier'); var RecentConnection = $injector.get('RecentConnection'); // Required services - var guacClientManager = $injector.get('guacClientManager'); var guacHistory = $injector.get('guacHistory'); - /** - * Array of all known and visible active connections. - * - * @type ActiveConnection[] - */ - $scope.activeConnections = []; - /** * Array of all known and visible recently-used connections. * @@ -68,16 +60,12 @@ angular.module('home').directive('guacRecentConnections', [function guacRecentCo /** * Returns whether recent connections are available for display. - * Note that, for the sake of this directive, recent connections - * include any currently-active connections, even if they are not - * yet in the history. * * @returns {Boolean} - * true if recent (or active) connections are present, false - * otherwise. + * true if recent connections are present, false otherwise. */ $scope.hasRecentConnections = function hasRecentConnections() { - return !!($scope.activeConnections.length || $scope.recentConnections.length); + return !!$scope.recentConnections.length; }; /** @@ -149,7 +137,6 @@ angular.module('home').directive('guacRecentConnections', [function guacRecentCo $scope.$watch("rootGroups", function setRootGroups(rootGroups) { // Clear connection arrays - $scope.activeConnections = []; $scope.recentConnections = []; // Produce collection of visible objects @@ -160,29 +147,11 @@ angular.module('home').directive('guacRecentConnections', [function guacRecentCo }); } - var managedClients = guacClientManager.getManagedClients(); - - // Add all active connections - for (var id in managedClients) { - - // Get corresponding managed client - var client = managedClients[id]; - - // Add active connections for clients with associated visible objects - if (id in visibleObjects) { - - var object = visibleObjects[id]; - $scope.activeConnections.push(new ActiveConnection(object.name, client)); - - } - - } - // Add any recent connections that are visible guacHistory.recentConnections.forEach(function addRecentConnection(historyEntry) { // Add recent connections for history entries with associated visible objects - if (historyEntry.id in visibleObjects && !(historyEntry.id in managedClients)) { + if (historyEntry.id in visibleObjects) { var object = visibleObjects[historyEntry.id]; $scope.recentConnections.push(new RecentConnection(object.name, historyEntry)); diff --git a/guacamole/src/main/frontend/src/app/home/templates/guacRecentConnections.html b/guacamole/src/main/frontend/src/app/home/templates/guacRecentConnections.html index b2be7a3bd..4654bd4d4 100644 --- a/guacamole/src/main/frontend/src/app/home/templates/guacRecentConnections.html +++ b/guacamole/src/main/frontend/src/app/home/templates/guacRecentConnections.html @@ -3,23 +3,6 @@

      {{'HOME.INFO_NO_RECENT_CONNECTIONS' | translate}}

      - - -
      diff --git a/guacamole/src/main/frontend/src/app/index/controllers/indexController.js b/guacamole/src/main/frontend/src/app/index/controllers/indexController.js index 51f129a3f..8e6413250 100644 --- a/guacamole/src/main/frontend/src/app/index/controllers/indexController.js +++ b/guacamole/src/main/frontend/src/app/index/controllers/indexController.js @@ -24,11 +24,12 @@ angular.module('index').controller('indexController', ['$scope', '$injector', function indexController($scope, $injector) { // Required services - var $document = $injector.get('$document'); - var $route = $injector.get('$route'); - var $window = $injector.get('$window'); - var clipboardService = $injector.get('clipboardService'); - var guacNotification = $injector.get('guacNotification'); + var $document = $injector.get('$document'); + var $route = $injector.get('$route'); + var $window = $injector.get('$window'); + var clipboardService = $injector.get('clipboardService'); + var guacNotification = $injector.get('guacNotification'); + var guacClientManager = $injector.get('guacClientManager'); /** * The error that prevents the current page from rendering at all. If no @@ -43,6 +44,14 @@ angular.module('index').controller('indexController', ['$scope', '$injector', */ $scope.guacNotification = guacNotification; + /** + * All currently-active connections, grouped into their corresponding + * tiled views. + * + * @type ManagedClientGroup[] + */ + $scope.getManagedClientGroups = guacClientManager.getManagedClientGroups; + /** * The message to display to the user as instructions for the login * process. diff --git a/guacamole/src/main/frontend/src/app/client/styles/other-connections.css b/guacamole/src/main/frontend/src/app/index/styles/other-connections.css similarity index 99% rename from guacamole/src/main/frontend/src/app/client/styles/other-connections.css rename to guacamole/src/main/frontend/src/app/index/styles/other-connections.css index 6c57aaaa8..36600992b 100644 --- a/guacamole/src/main/frontend/src/app/client/styles/other-connections.css +++ b/guacamole/src/main/frontend/src/app/index/styles/other-connections.css @@ -20,7 +20,7 @@ #other-connections .client-panel { display: none; - position: absolute; + position: fixed; right: 0; bottom: 0; diff --git a/guacamole/src/main/frontend/src/index.html b/guacamole/src/main/frontend/src/index.html index 70bca9f9d..2a5f6e0f1 100644 --- a/guacamole/src/main/frontend/src/index.html +++ b/guacamole/src/main/frontend/src/index.html @@ -80,6 +80,11 @@
      + +
      + +
      +