From 85d01ba730ef59d6bb09d2cf7b9f2ce07f12ef7f Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Sun, 17 Jan 2021 16:15:50 -0800 Subject: [PATCH 01/58] GUACAMOLE-724: Implement base support for displaying multiple connections in a tiled grid. --- .../client/controllers/clientController.js | 39 ++++- .../src/app/client/directives/guacClient.js | 10 +- .../app/client/directives/guacTiledClients.js | 145 ++++++++++++++++++ .../frontend/src/app/client/styles/client.css | 2 +- .../app/client/styles/tiled-client-list.css | 60 ++++++++ .../src/app/client/templates/client.html | 4 +- .../src/app/client/templates/connection.html | 1 + .../app/client/templates/connectionGroup.html | 1 + .../client/templates/guacTiledClients.html | 13 ++ .../src/app/client/types/ClientProperties.js | 12 +- 10 files changed, 272 insertions(+), 15 deletions(-) create mode 100644 guacamole/src/main/frontend/src/app/client/directives/guacTiledClients.js create mode 100644 guacamole/src/main/frontend/src/app/client/styles/tiled-client-list.css create mode 100644 guacamole/src/main/frontend/src/app/client/templates/guacTiledClients.html 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 1bc5dd6fa..389170745 100644 --- a/guacamole/src/main/frontend/src/app/client/controllers/clientController.js +++ b/guacamole/src/main/frontend/src/app/client/controllers/clientController.js @@ -279,20 +279,37 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams /** * The client which should be attached to the client UI. * - * @type ManagedClient + * @type ManagedClient[] */ - $scope.client = guacClientManager.getManagedClient($routeParams.id, $routeParams.params); + $scope.clients = (function getClients() { + + var clients = []; + + var ids = $routeParams.id.split(/[ +]/); + ids.forEach(function addClient(id) { + clients.push(guacClientManager.getManagedClient(id, $routeParams.params)); + }); + + return clients; + + })(); /** - * All active clients which are not the current client ($scope.client). + * 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. */ $scope.otherClients = (function getOtherClients(clients) { + var otherClients = angular.extend({}, clients); - delete otherClients[$scope.client.id]; + + $scope.clients.forEach(function removeActiveCLient(client) { + delete otherClients[client.id]; + }); + return otherClients; + })(guacClientManager.getManagedClients()); /** @@ -526,7 +543,9 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams $scope.menu.connectionParameters = ManagedClient.getArgumentModel($scope.client); // Disable client keyboard if the menu is shown - $scope.client.clientProperties.keyboardEnabled = !menuShown; + angular.forEach($scope.clients, function updateKeyboardEnabled(client) { + client.clientProperties.keyboardEnabled = !menuShown; + }); }); @@ -731,7 +750,15 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams * otherwise. */ $scope.isConnectionUnstable = function isConnectionUnstable() { - return $scope.client && $scope.client.clientState.tunnelUnstable; + + var unstable = false; + + angular.forEach($scope.clients, function checkStability(client) { + unstable |= client.clientState.tunnelUnstable; + }); + + return unstable; + }; /** diff --git a/guacamole/src/main/frontend/src/app/client/directives/guacClient.js b/guacamole/src/main/frontend/src/app/client/directives/guacClient.js index 841d39271..6e47c26b2 100644 --- a/guacamole/src/main/frontend/src/app/client/directives/guacClient.js +++ b/guacamole/src/main/frontend/src/app/client/directives/guacClient.js @@ -421,7 +421,7 @@ angular.module('client').directive('guacClient', [function guacClient() { // Translate local keydown events to remote keydown events if keyboard is enabled $scope.$on('guacKeydown', function keydownListener(event, keysym, keyboard) { - if ($scope.client.clientProperties.keyboardEnabled && !event.defaultPrevented) { + if ($scope.client.clientProperties.keyboardEnabled && $scope.client.clientProperties.focused) { client.sendKeyEvent(1, keysym); event.preventDefault(); } @@ -429,7 +429,7 @@ angular.module('client').directive('guacClient', [function guacClient() { // Translate local keyup events to remote keyup events if keyboard is enabled $scope.$on('guacKeyup', function keyupListener(event, keysym, keyboard) { - if ($scope.client.clientProperties.keyboardEnabled && !event.defaultPrevented) { + if ($scope.client.clientProperties.keyboardEnabled && $scope.client.clientProperties.focused) { client.sendKeyEvent(0, keysym); event.preventDefault(); } @@ -437,12 +437,14 @@ angular.module('client').directive('guacClient', [function guacClient() { // Universally handle all synthetic keydown events $scope.$on('guacSyntheticKeydown', function syntheticKeydownListener(event, keysym) { - client.sendKeyEvent(1, keysym); + if ($scope.client.clientProperties.focused) + client.sendKeyEvent(1, keysym); }); // Universally handle all synthetic keyup events $scope.$on('guacSyntheticKeyup', function syntheticKeyupListener(event, keysym) { - client.sendKeyEvent(0, keysym); + if ($scope.client.clientProperties.focused) + client.sendKeyEvent(0, keysym); }); /** diff --git a/guacamole/src/main/frontend/src/app/client/directives/guacTiledClients.js b/guacamole/src/main/frontend/src/app/client/directives/guacTiledClients.js new file mode 100644 index 000000000..83f8b25e6 --- /dev/null +++ b/guacamole/src/main/frontend/src/app/client/directives/guacTiledClients.js @@ -0,0 +1,145 @@ +/* + * 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 which displays one or more Guacamole clients in an evenly-tiled + * view. The number of rows and columns used for the arrangement of tiles is + * automatically determined by the number of clients present. + */ +angular.module('client').directive('guacTiledClients', [function guacTiledClients() { + + var directive = { + restrict: 'E', + templateUrl: 'app/client/templates/guacTiledClients.html', + }; + + directive.scope = { + + /** + * The Guacamole clients that should be displayed in an evenly-tiled + * grid arrangement. + * + * @type ManagedClient[] + */ + clients : '=' + + }; + + 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()); + + }; + + /** + * Assigns keyboard focus to the given client, allowing that client to + * receive and handle keyboard events. Multiple clients may have + * keyboard focus simultaneously. + * + * @param {ManagedClient} client + * The client that should receive keyboard focus. + */ + $scope.assignFocus = function assignFocus(client) { + client.clientProperties.focused = true; + }; + + /** + * Returns whether multiple clients are currently shown within the + * tiled grid. + * + * @returns {Boolean} + * true if two or more clients are currently present, false + * otherwise. + */ + $scope.hasMultipleClients = function hasMultipleClients() { + return $scope.clients && $scope.clients.length > 1; + }; + + /** + * 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 / getColumns()) + '%'; + }; + + /** + * 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 / 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 directive; + +}]); diff --git a/guacamole/src/main/frontend/src/app/client/styles/client.css b/guacamole/src/main/frontend/src/app/client/styles/client.css index 9ec74b6b2..16bd2d4a7 100644 --- a/guacamole/src/main/frontend/src/app/client/styles/client.css +++ b/guacamole/src/main/frontend/src/app/client/styles/client.css @@ -103,7 +103,7 @@ body.client { flex: 0 0 auto; } -.client-view .client-body .main { +.client-view .client-body .tiled-client-list { position: absolute; left: 0; diff --git a/guacamole/src/main/frontend/src/app/client/styles/tiled-client-list.css b/guacamole/src/main/frontend/src/app/client/styles/tiled-client-list.css new file mode 100644 index 000000000..1b89de080 --- /dev/null +++ b/guacamole/src/main/frontend/src/app/client/styles/tiled-client-list.css @@ -0,0 +1,60 @@ +/* + * 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. + */ + +.tiled-client-list { + padding: 0; + margin: 0; + line-height: 0; +} + +.tiled-client-list li.client-tile { + position: relative; + display: inline-flex; + flex-direction: column; + line-height: 1.5; +} + +.tiled-client-list li.client-tile h3 { + margin: 0; + background: #444; + padding: 0 0.25em; + font-size: 0.8em; + color: white; + display: none; +} + +.tiled-client-list.multiple-clients li.client-tile h3 { + display: block; +} + +.tiled-client-list.multiple-clients li.client-tile { + border: 1px solid #444; +} + +.tiled-client-list li.client-tile.focused { + border-color: #3161a9; +} + +.tiled-client-list li.client-tile.focused h3 { + background-color: #3161a9; +} + +.tiled-client-list li.client-tile .main { + flex: 1; +} 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 edc5cb665..17f0c502f 100644 --- a/guacamole/src/main/frontend/src/app/client/templates/client.html +++ b/guacamole/src/main/frontend/src/app/client/templates/client.html @@ -8,8 +8,8 @@
- - + +
diff --git a/guacamole/src/main/frontend/src/app/client/templates/connection.html b/guacamole/src/main/frontend/src/app/client/templates/connection.html index 7ddc72bfa..5778f81a0 100644 --- a/guacamole/src/main/frontend/src/app/client/templates/connection.html +++ b/guacamole/src/main/frontend/src/app/client/templates/connection.html @@ -1,4 +1,5 @@
+ {{item.name}}
diff --git a/guacamole/src/main/frontend/src/app/client/templates/connectionGroup.html b/guacamole/src/main/frontend/src/app/client/templates/connectionGroup.html index 680491c3b..5b33b7a8c 100644 --- a/guacamole/src/main/frontend/src/app/client/templates/connectionGroup.html +++ b/guacamole/src/main/frontend/src/app/client/templates/connectionGroup.html @@ -1,4 +1,5 @@
+ {{item.name}}
diff --git a/guacamole/src/main/frontend/src/app/client/templates/guacTiledClients.html b/guacamole/src/main/frontend/src/app/client/templates/guacTiledClients.html new file mode 100644 index 000000000..f1ceb64ac --- /dev/null +++ b/guacamole/src/main/frontend/src/app/client/templates/guacTiledClients.html @@ -0,0 +1,13 @@ +
    + +
  • + +

    {{ getClientTitle(client) }}

    + +
  • + +
\ No newline at end of file diff --git a/guacamole/src/main/frontend/src/app/client/types/ClientProperties.js b/guacamole/src/main/frontend/src/app/client/types/ClientProperties.js index d5940e2be..a138812b2 100644 --- a/guacamole/src/main/frontend/src/app/client/types/ClientProperties.js +++ b/guacamole/src/main/frontend/src/app/client/types/ClientProperties.js @@ -69,12 +69,20 @@ angular.module('client').factory('ClientProperties', ['$injector', function defi this.maxScale = template.maxScale || 3; /** - * Whether or not the client should listen to keyboard events. + * Whether this client should listen to keyboard events that it + * receives. * * @type Boolean */ this.keyboardEnabled = template.keyboardEnabled || true; - + + /** + * Whether this client should receive keyboard events. + * + * @type Boolean + */ + this.focused = template.focused || false; + /** * Whether translation of touch to mouse events should emulate an * absolute pointer device, or a relative pointer device. From fbdb692444e66533c9893e63d650c22f4a2818d2 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Tue, 1 Jun 2021 17:07:40 -0700 Subject: [PATCH 02/58] GUACAMOLE-724: Remove unused arbitrary parameters from client route (the webapp tunnel doesn't use these). --- .../app/client/controllers/clientController.js | 4 ++-- .../app/client/services/guacClientManager.js | 18 ++++-------------- .../src/app/client/types/ManagedClient.js | 16 ++++------------ .../src/app/index/config/indexRouteConfig.js | 2 +- 4 files changed, 11 insertions(+), 29 deletions(-) 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 389170745..c391b3b2f 100644 --- a/guacamole/src/main/frontend/src/app/client/controllers/clientController.js +++ b/guacamole/src/main/frontend/src/app/client/controllers/clientController.js @@ -201,7 +201,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams name : "CLIENT.ACTION_RECONNECT", className : "reconnect button", callback : function reconnectCallback() { - $scope.client = guacClientManager.replaceManagedClient($routeParams.id, $routeParams.params); + $scope.client = guacClientManager.replaceManagedClient($routeParams.id); guacNotification.showStatus(false); } }; @@ -287,7 +287,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams var ids = $routeParams.id.split(/[ +]/); ids.forEach(function addClient(id) { - clients.push(guacClientManager.getManagedClient(id, $routeParams.params)); + clients.push(guacClientManager.getManagedClient(id)); }); return clients; 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 4b81e6d19..71918ea67 100644 --- a/guacamole/src/main/frontend/src/app/client/services/guacClientManager.js +++ b/guacamole/src/main/frontend/src/app/client/services/guacClientManager.js @@ -96,22 +96,17 @@ angular.module('client').factory('guacClientManager', ['$injector', * @param {String} id * The ID of the connection whose ManagedClient should be retrieved. * - * @param {String} [connectionParameters] - * Any additional HTTP parameters to pass while connecting. This - * parameter only has an effect if a new connection is established as - * a result of this function call. - * * @returns {ManagedClient} * The ManagedClient associated with the connection having the given * ID. */ - service.replaceManagedClient = function replaceManagedClient(id, connectionParameters) { + service.replaceManagedClient = function replaceManagedClient(id) { // Disconnect any existing client service.removeManagedClient(id); // Set new client - return storedManagedClients()[id] = ManagedClient.getInstance(id, connectionParameters); + return storedManagedClients()[id] = ManagedClient.getInstance(id); }; @@ -123,22 +118,17 @@ angular.module('client').factory('guacClientManager', ['$injector', * @param {String} id * The ID of the connection whose ManagedClient should be retrieved. * - * @param {String} [connectionParameters] - * Any additional HTTP parameters to pass while connecting. This - * parameter only has an effect if a new connection is established as - * a result of this function call. - * * @returns {ManagedClient} * The ManagedClient associated with the connection having the given * ID. */ - service.getManagedClient = function getManagedClient(id, connectionParameters) { + service.getManagedClient = function getManagedClient(id) { var managedClients = storedManagedClients(); // Create new managed client if it doesn't already exist if (!(id in managedClients)) - managedClients[id] = ManagedClient.getInstance(id, connectionParameters); + managedClients[id] = ManagedClient.getInstance(id); // Return existing client return managedClients[id]; 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 51c38a3ed..4e59b6ae5 100644 --- a/guacamole/src/main/frontend/src/app/client/types/ManagedClient.js +++ b/guacamole/src/main/frontend/src/app/client/types/ManagedClient.js @@ -46,7 +46,6 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector', var connectionService = $injector.get('connectionService'); var preferenceService = $injector.get('preferenceService'); var requestService = $injector.get('requestService'); - var schemaService = $injector.get('schemaService'); var tunnelService = $injector.get('tunnelService'); var guacAudio = $injector.get('guacAudio'); var guacHistory = $injector.get('guacHistory'); @@ -260,14 +259,11 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector', * @param {ClientIdentifier} identifier * The identifier representing the connection or group to connect to. * - * @param {String} [connectionParameters] - * Any additional HTTP parameters to pass while connecting. - * * @returns {Promise.} * A promise which resolves with the string of connection parameters to * be passed to the Guacamole client, once the string is ready. */ - var getConnectString = function getConnectString(identifier, connectionParameters) { + var getConnectString = function getConnectString(identifier) { var deferred = $q.defer(); @@ -286,8 +282,7 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector', + "&GUAC_WIDTH=" + Math.floor(optimal_width) + "&GUAC_HEIGHT=" + Math.floor(optimal_height) + "&GUAC_DPI=" + Math.floor(optimal_dpi) - + "&GUAC_TIMEZONE=" + encodeURIComponent(preferenceService.preferences.timezone) - + (connectionParameters ? '&' + connectionParameters : ''); + + "&GUAC_TIMEZONE=" + encodeURIComponent(preferenceService.preferences.timezone); // Add audio mimetypes to connect string guacAudio.supported.forEach(function(mimetype) { @@ -355,14 +350,11 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector', * a valid ClientIdentifier string, as would be generated by * ClientIdentifier.toString(). * - * @param {String} [connectionParameters] - * Any additional HTTP parameters to pass while connecting. - * * @returns {ManagedClient} * A new ManagedClient instance which is connected to the connection or * connection group having the given ID. */ - ManagedClient.getInstance = function getInstance(id, connectionParameters) { + ManagedClient.getInstance = function getInstance(id) { var tunnel; @@ -628,7 +620,7 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector', var clientIdentifier = ClientIdentifier.fromString(id); // Connect the Guacamole client - getConnectString(clientIdentifier, connectionParameters) + getConnectString(clientIdentifier) .then(function connectClient(connectString) { client.connect(connectString); }); diff --git a/guacamole/src/main/frontend/src/app/index/config/indexRouteConfig.js b/guacamole/src/main/frontend/src/app/index/config/indexRouteConfig.js index 5a8c3fb8a..684114514 100644 --- a/guacamole/src/main/frontend/src/app/index/config/indexRouteConfig.js +++ b/guacamole/src/main/frontend/src/app/index/config/indexRouteConfig.js @@ -181,7 +181,7 @@ angular.module('index').config(['$routeProvider', '$locationProvider', }) // Client view - .when('/client/:id/:params?', { + .when('/client/:id', { bodyClassName : 'client', templateUrl : 'app/client/templates/client.html', controller : 'clientController', From 2f1d46aa8671da1fbe743c42da86f6ad7bb5cf88 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Tue, 1 Jun 2021 17:34:08 -0700 Subject: [PATCH 03/58] GUACAMOLE-724: Provide separate, client-specific notifications for each tiled client. --- .../client/controllers/clientController.js | 359 --------------- .../directives/guacClientNotification.js | 431 ++++++++++++++++++ .../src/app/client/styles/notification.css | 57 ++- .../templates/guacClientNotification.html | 5 + .../client/templates/guacTiledClients.html | 4 + 5 files changed, 495 insertions(+), 361 deletions(-) create mode 100644 guacamole/src/main/frontend/src/app/client/directives/guacClientNotification.js create mode 100644 guacamole/src/main/frontend/src/app/client/templates/guacClientNotification.html 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 c391b3b2f..c19aa594e 100644 --- a/guacamole/src/main/frontend/src/app/client/controllers/clientController.js +++ b/guacamole/src/main/frontend/src/app/client/controllers/clientController.js @@ -32,13 +32,11 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams var ScrollState = $injector.get('ScrollState'); // Required services - var $location = $injector.get('$location'); var authenticationService = $injector.get('authenticationService'); var connectionGroupService = $injector.get('connectionGroupService'); var clipboardService = $injector.get('clipboardService'); var dataSourceService = $injector.get('dataSourceService'); var guacClientManager = $injector.get('guacClientManager'); - var guacNotification = $injector.get('guacNotification'); var iconService = $injector.get('iconService'); var preferenceService = $injector.get('preferenceService'); var requestService = $injector.get('requestService'); @@ -93,129 +91,6 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams */ var DEL_KEY = 0xFFFF; - /** - * All client error codes handled and passed off for translation. Any error - * code not present in this list will be represented by the "DEFAULT" - * translation. - */ - var CLIENT_ERRORS = { - 0x0201: true, - 0x0202: true, - 0x0203: true, - 0x0207: true, - 0x0208: true, - 0x0209: true, - 0x020A: true, - 0x020B: true, - 0x0301: true, - 0x0303: true, - 0x0308: true, - 0x031D: true - }; - - /** - * All error codes for which automatic reconnection is appropriate when a - * client error occurs. - */ - var CLIENT_AUTO_RECONNECT = { - 0x0200: true, - 0x0202: true, - 0x0203: true, - 0x0207: true, - 0x0208: true, - 0x0301: true, - 0x0308: true - }; - - /** - * All tunnel error codes handled and passed off for translation. Any error - * code not present in this list will be represented by the "DEFAULT" - * translation. - */ - var TUNNEL_ERRORS = { - 0x0201: true, - 0x0202: true, - 0x0203: true, - 0x0204: true, - 0x0205: true, - 0x0207: true, - 0x0208: true, - 0x0301: true, - 0x0303: true, - 0x0308: true, - 0x031D: true - }; - - /** - * All error codes for which automatic reconnection is appropriate when a - * tunnel error occurs. - */ - var TUNNEL_AUTO_RECONNECT = { - 0x0200: true, - 0x0202: true, - 0x0203: true, - 0x0207: true, - 0x0208: true, - 0x0308: true - }; - - /** - * Action which logs out from Guacamole entirely. - */ - var LOGOUT_ACTION = { - name : "CLIENT.ACTION_LOGOUT", - className : "logout button", - callback : function logoutCallback() { - authenticationService.logout() - ['catch'](requestService.IGNORE); - } - }; - - /** - * Action which returns the user to the home screen. If the home page has - * not yet been determined, this will be null. - */ - var NAVIGATE_HOME_ACTION = null; - - // Assign home page action once user's home page has been determined - userPageService.getHomePage() - .then(function homePageRetrieved(homePage) { - - // Define home action only if different from current location - if ($location.path() !== homePage.url) { - NAVIGATE_HOME_ACTION = { - name : "CLIENT.ACTION_NAVIGATE_HOME", - className : "home button", - callback : function navigateHomeCallback() { - $location.url(homePage.url); - } - }; - } - - }, requestService.WARN); - - /** - * Action which replaces the current client with a newly-connected client. - */ - var RECONNECT_ACTION = { - name : "CLIENT.ACTION_RECONNECT", - className : "reconnect button", - callback : function reconnectCallback() { - $scope.client = guacClientManager.replaceManagedClient($routeParams.id); - guacNotification.showStatus(false); - } - }; - - /** - * The reconnect countdown to display if an error or status warrants an - * automatic, timed reconnect. - */ - var RECONNECT_COUNTDOWN = { - text: "CLIENT.TEXT_RECONNECT_COUNTDOWN", - callback: RECONNECT_ACTION.callback, - remaining: 15 - }; - /** * Menu-specific properties. */ @@ -716,31 +591,6 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams $scope.page.title = title; }); - /** - * Displays a notification at the end of a Guacamole connection, whether - * that connection is ending normally or due to an error. As the end of - * a Guacamole connection may be due to changes in authentication status, - * this will also implicitly peform a re-authentication attempt to check - * for such changes, possibly resulting in auth-related events like - * guacInvalidCredentials. - * - * @param {Notification|Boolean|Object} status - * The status notification to show, as would be accepted by - * guacNotification.showStatus(). - */ - var notifyConnectionClosed = function notifyConnectionClosed(status) { - - // Re-authenticate to verify auth status at end of connection - authenticationService.updateCurrentToken($location.search()) - ['catch'](requestService.IGNORE) - - // Show the requested status once the authentication check has finished - ['finally'](function authenticationCheckComplete() { - guacNotification.showStatus(status); - }); - - }; - /** * Returns whether the current connection has been flagged as unstable due * to an apparent network disruption. @@ -761,215 +611,6 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams }; - /** - * Notifies the user that the connection state has changed. - * - * @param {String} connectionState - * The current connection state, as defined by - * ManagedClientState.ConnectionState. - */ - var notifyConnectionState = function notifyConnectionState(connectionState) { - - // Hide any existing status - guacNotification.showStatus(false); - - // Do not display status if status not known - if (!connectionState) - return; - - // Build array of available actions - var actions; - if (NAVIGATE_HOME_ACTION) - actions = [ NAVIGATE_HOME_ACTION, RECONNECT_ACTION, LOGOUT_ACTION ]; - else - actions = [ RECONNECT_ACTION, LOGOUT_ACTION ]; - - // Get any associated status code - var status = $scope.client.clientState.statusCode; - - // Connecting - if (connectionState === ManagedClientState.ConnectionState.CONNECTING - || connectionState === ManagedClientState.ConnectionState.WAITING) { - guacNotification.showStatus({ - title: "CLIENT.DIALOG_HEADER_CONNECTING", - text: { - key : "CLIENT.TEXT_CLIENT_STATUS_" + connectionState.toUpperCase() - } - }); - } - - // Client error - else if (connectionState === ManagedClientState.ConnectionState.CLIENT_ERROR) { - - // Determine translation name of error - var errorName = (status in CLIENT_ERRORS) ? status.toString(16).toUpperCase() : "DEFAULT"; - - // Determine whether the reconnect countdown applies - var countdown = (status in CLIENT_AUTO_RECONNECT) ? RECONNECT_COUNTDOWN : null; - - // Show error status - notifyConnectionClosed({ - className : "error", - title : "CLIENT.DIALOG_HEADER_CONNECTION_ERROR", - text : { - key : "CLIENT.ERROR_CLIENT_" + errorName - }, - countdown : countdown, - actions : actions - }); - - } - - // Tunnel error - else if (connectionState === ManagedClientState.ConnectionState.TUNNEL_ERROR) { - - // Determine translation name of error - var errorName = (status in TUNNEL_ERRORS) ? status.toString(16).toUpperCase() : "DEFAULT"; - - // Determine whether the reconnect countdown applies - var countdown = (status in TUNNEL_AUTO_RECONNECT) ? RECONNECT_COUNTDOWN : null; - - // Show error status - notifyConnectionClosed({ - className : "error", - title : "CLIENT.DIALOG_HEADER_CONNECTION_ERROR", - text : { - key : "CLIENT.ERROR_TUNNEL_" + errorName - }, - countdown : countdown, - actions : actions - }); - - } - - // Disconnected - else if (connectionState === ManagedClientState.ConnectionState.DISCONNECTED) { - notifyConnectionClosed({ - title : "CLIENT.DIALOG_HEADER_DISCONNECTED", - text : { - key : "CLIENT.TEXT_CLIENT_STATUS_" + connectionState.toUpperCase() - }, - actions : actions - }); - } - - // Hide status and sync local clipboard once connected - else if (connectionState === ManagedClientState.ConnectionState.CONNECTED) { - - // Sync with local clipboard - clipboardService.getLocalClipboard().then(function clipboardRead(data) { - $scope.$broadcast('guacClipboard', data); - }, angular.noop); - - // Hide status notification - guacNotification.showStatus(false); - - } - - // Hide status for all other states - else - guacNotification.showStatus(false); - - }; - - /** - * Prompts the user to enter additional connection parameters. If the - * protocol and associated parameters of the underlying connection are not - * yet known, this function has no effect and should be re-invoked once - * the parameters are known. - * - * @param {Object.} requiredParameters - * The set of all parameters requested by the server via "required" - * instructions, where each object key is the name of a requested - * parameter and each value is the current value entered by the user. - */ - var notifyParametersRequired = function notifyParametersRequired(requiredParameters) { - - /** - * Action which submits the current set of parameter values, requesting - * that the connection continue. - */ - var SUBMIT_PARAMETERS = { - name : "CLIENT.ACTION_CONTINUE", - className : "button", - callback : function submitParameters() { - if ($scope.client) { - var params = $scope.client.requiredParameters; - $scope.client.requiredParameters = null; - ManagedClient.sendArguments($scope.client, params); - } - } - }; - - /** - * Action which cancels submission of additional parameters and - * disconnects from the current connection. - */ - var CANCEL_PARAMETER_SUBMISSION = { - name : "CLIENT.ACTION_CANCEL", - className : "button", - callback : function cancelSubmission() { - $scope.client.requiredParameters = null; - $scope.disconnect(); - } - }; - - // Attempt to prompt for parameters only if the parameters that apply - // to the underlying connection are known - if (!$scope.client.protocol || !$scope.client.forms) - return; - - // Hide any existing status - guacNotification.showStatus(false); - - // Prompt for parameters - guacNotification.showStatus({ - formNamespace : Protocol.getNamespace($scope.client.protocol), - forms : $scope.client.forms, - formModel : requiredParameters, - formSubmitCallback : SUBMIT_PARAMETERS.callback, - actions : [ SUBMIT_PARAMETERS, CANCEL_PARAMETER_SUBMISSION ] - }); - - }; - - /** - * Returns whether the given connection state allows for submission of - * connection parameters via "argv" instructions. - * - * @param {String} connectionState - * The connection state to test, as defined by - * ManagedClientState.ConnectionState. - * - * @returns {boolean} - * true if the given connection state allows submission of connection - * parameters via "argv" instructions, false otherwise. - */ - var canSubmitParameters = function canSubmitParameters(connectionState) { - return (connectionState === ManagedClientState.ConnectionState.WAITING || - connectionState === ManagedClientState.ConnectionState.CONNECTED); - }; - - // Show status dialog when connection status changes - $scope.$watchGroup([ - 'client.clientState.connectionState', - 'client.requiredParameters', - 'client.protocol', - 'client.forms' - ], function clientStateChanged(newValues) { - - var connectionState = newValues[0]; - var requiredParameters = newValues[1]; - - // Prompt for parameters only if parameters can actually be submitted - if (requiredParameters && canSubmitParameters(connectionState)) - notifyParametersRequired(requiredParameters); - - // Otherwise, just show general connection state - else - notifyConnectionState(connectionState); - - }); $scope.zoomIn = function zoomIn() { $scope.menu.autoFit = false; diff --git a/guacamole/src/main/frontend/src/app/client/directives/guacClientNotification.js b/guacamole/src/main/frontend/src/app/client/directives/guacClientNotification.js new file mode 100644 index 000000000..9021b0b7b --- /dev/null +++ b/guacamole/src/main/frontend/src/app/client/directives/guacClientNotification.js @@ -0,0 +1,431 @@ +/* + * 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 non-global notification describing the status + * of a specific Guacamole client, including prompts for any information + * necessary to continue the connection. + */ +angular.module('client').directive('guacClientNotification', [function guacClientNotification() { + + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'app/client/templates/guacClientNotification.html' + }; + + directive.scope = { + + /** + * The client whose status should be displayed. + * + * @type ManagedClient + */ + client : '=' + + }; + + directive.controller = ['$scope', '$injector', '$element', + function guacClientNotificationController($scope, $injector, $element) { + + // Required types + var ManagedClient = $injector.get('ManagedClient'); + var ManagedClientState = $injector.get('ManagedClientState'); + var Protocol = $injector.get('Protocol'); + + // Required services + var $location = $injector.get('$location'); + var authenticationService = $injector.get('authenticationService'); + var guacClientManager = $injector.get('guacClientManager'); + var requestService = $injector.get('requestService'); + var userPageService = $injector.get('userPageService'); + + /** + * A Notification object describing the client status to display as a + * dialog or prompt, as would be accepted by guacNotification.showStatus(), + * or false if no status should be shown. + * + * @type {Notification|Object|Boolean} + */ + $scope.status = false; + + /** + * All client error codes handled and passed off for translation. Any error + * code not present in this list will be represented by the "DEFAULT" + * translation. + */ + var CLIENT_ERRORS = { + 0x0201: true, + 0x0202: true, + 0x0203: true, + 0x0207: true, + 0x0208: true, + 0x0209: true, + 0x020A: true, + 0x020B: true, + 0x0301: true, + 0x0303: true, + 0x0308: true, + 0x031D: true + }; + + /** + * All error codes for which automatic reconnection is appropriate when a + * client error occurs. + */ + var CLIENT_AUTO_RECONNECT = { + 0x0200: true, + 0x0202: true, + 0x0203: true, + 0x0207: true, + 0x0208: true, + 0x0301: true, + 0x0308: true + }; + + /** + * All tunnel error codes handled and passed off for translation. Any error + * code not present in this list will be represented by the "DEFAULT" + * translation. + */ + var TUNNEL_ERRORS = { + 0x0201: true, + 0x0202: true, + 0x0203: true, + 0x0204: true, + 0x0205: true, + 0x0207: true, + 0x0208: true, + 0x0301: true, + 0x0303: true, + 0x0308: true, + 0x031D: true + }; + + /** + * All error codes for which automatic reconnection is appropriate when a + * tunnel error occurs. + */ + var TUNNEL_AUTO_RECONNECT = { + 0x0200: true, + 0x0202: true, + 0x0203: true, + 0x0207: true, + 0x0208: true, + 0x0308: true + }; + + /** + * Action which logs out from Guacamole entirely. + */ + var LOGOUT_ACTION = { + name : "CLIENT.ACTION_LOGOUT", + className : "logout button", + callback : function logoutCallback() { + authenticationService.logout() + ['catch'](requestService.IGNORE); + } + }; + + /** + * Action which returns the user to the home screen. If the home page has + * not yet been determined, this will be null. + */ + var NAVIGATE_HOME_ACTION = null; + + // Assign home page action once user's home page has been determined + userPageService.getHomePage() + .then(function homePageRetrieved(homePage) { + + // Define home action only if different from current location + if ($location.path() !== homePage.url) { + NAVIGATE_HOME_ACTION = { + name : "CLIENT.ACTION_NAVIGATE_HOME", + className : "home button", + callback : function navigateHomeCallback() { + $location.url(homePage.url); + } + }; + } + + }, requestService.WARN); + + /** + * Action which replaces the current client with a newly-connected client. + */ + var RECONNECT_ACTION = { + name : "CLIENT.ACTION_RECONNECT", + className : "reconnect button", + callback : function reconnectCallback() { + $scope.client = guacClientManager.replaceManagedClient($scope.client.id); + $scope.status = false; + } + }; + + /** + * The reconnect countdown to display if an error or status warrants an + * automatic, timed reconnect. + */ + var RECONNECT_COUNTDOWN = { + text: "CLIENT.TEXT_RECONNECT_COUNTDOWN", + callback: RECONNECT_ACTION.callback, + remaining: 15 + }; + + /** + * Displays a notification at the end of a Guacamole connection, whether + * that connection is ending normally or due to an error. As the end of + * a Guacamole connection may be due to changes in authentication status, + * this will also implicitly peform a re-authentication attempt to check + * for such changes, possibly resulting in auth-related events like + * guacInvalidCredentials. + * + * @param {Notification|Boolean|Object} status + * The status notification to show, as would be accepted by + * guacNotification.showStatus(). + */ + var notifyConnectionClosed = function notifyConnectionClosed(status) { + + // Re-authenticate to verify auth status at end of connection + authenticationService.updateCurrentToken($location.search()) + ['catch'](requestService.IGNORE) + + // Show the requested status once the authentication check has finished + ['finally'](function authenticationCheckComplete() { + $scope.status = status; + }); + + }; + + /** + * Notifies the user that the connection state has changed. + * + * @param {String} connectionState + * The current connection state, as defined by + * ManagedClientState.ConnectionState. + */ + var notifyConnectionState = function notifyConnectionState(connectionState) { + + // Hide any existing status + $scope.status = false; + + // Do not display status if status not known + if (!connectionState) + return; + + // Build array of available actions + var actions; + if (NAVIGATE_HOME_ACTION) + actions = [ NAVIGATE_HOME_ACTION, RECONNECT_ACTION, LOGOUT_ACTION ]; + else + actions = [ RECONNECT_ACTION, LOGOUT_ACTION ]; + + // Get any associated status code + var status = $scope.client.clientState.statusCode; + + // Connecting + if (connectionState === ManagedClientState.ConnectionState.CONNECTING + || connectionState === ManagedClientState.ConnectionState.WAITING) { + $scope.status = { + title: "CLIENT.DIALOG_HEADER_CONNECTING", + text: { + key : "CLIENT.TEXT_CLIENT_STATUS_" + connectionState.toUpperCase() + } + }; + } + + // Client error + else if (connectionState === ManagedClientState.ConnectionState.CLIENT_ERROR) { + + // Determine translation name of error + var errorName = (status in CLIENT_ERRORS) ? status.toString(16).toUpperCase() : "DEFAULT"; + + // Determine whether the reconnect countdown applies + var countdown = (status in CLIENT_AUTO_RECONNECT) ? RECONNECT_COUNTDOWN : null; + + // Show error status + notifyConnectionClosed({ + className : "error", + title : "CLIENT.DIALOG_HEADER_CONNECTION_ERROR", + text : { + key : "CLIENT.ERROR_CLIENT_" + errorName + }, + countdown : countdown, + actions : actions + }); + + } + + // Tunnel error + else if (connectionState === ManagedClientState.ConnectionState.TUNNEL_ERROR) { + + // Determine translation name of error + var errorName = (status in TUNNEL_ERRORS) ? status.toString(16).toUpperCase() : "DEFAULT"; + + // Determine whether the reconnect countdown applies + var countdown = (status in TUNNEL_AUTO_RECONNECT) ? RECONNECT_COUNTDOWN : null; + + // Show error status + notifyConnectionClosed({ + className : "error", + title : "CLIENT.DIALOG_HEADER_CONNECTION_ERROR", + text : { + key : "CLIENT.ERROR_TUNNEL_" + errorName + }, + countdown : countdown, + actions : actions + }); + + } + + // Disconnected + else if (connectionState === ManagedClientState.ConnectionState.DISCONNECTED) { + notifyConnectionClosed({ + title : "CLIENT.DIALOG_HEADER_DISCONNECTED", + text : { + key : "CLIENT.TEXT_CLIENT_STATUS_" + connectionState.toUpperCase() + }, + actions : actions + }); + } + + // Hide status and sync local clipboard once connected + else if (connectionState === ManagedClientState.ConnectionState.CONNECTED) { + + // TODO: Move clipboard sync elsewhere + + // Sync with local clipboard + /* + clipboardService.getLocalClipboard().then(function clipboardRead(data) { + $scope.$broadcast('guacClipboard', data); + }, angular.noop); + */ + + // Hide status notification + $scope.status = false; + + } + + // Hide status for all other states + else + $scope.status = false; + + }; + + /** + * Prompts the user to enter additional connection parameters. If the + * protocol and associated parameters of the underlying connection are not + * yet known, this function has no effect and should be re-invoked once + * the parameters are known. + * + * @param {Object.} requiredParameters + * The set of all parameters requested by the server via "required" + * instructions, where each object key is the name of a requested + * parameter and each value is the current value entered by the user. + */ + var notifyParametersRequired = function notifyParametersRequired(requiredParameters) { + + /** + * Action which submits the current set of parameter values, requesting + * that the connection continue. + */ + var SUBMIT_PARAMETERS = { + name : "CLIENT.ACTION_CONTINUE", + className : "button", + callback : function submitParameters() { + if ($scope.client) { + var params = $scope.client.requiredParameters; + $scope.client.requiredParameters = null; + ManagedClient.sendArguments($scope.client, params); + } + } + }; + + /** + * Action which cancels submission of additional parameters and + * disconnects from the current connection. + */ + var CANCEL_PARAMETER_SUBMISSION = { + name : "CLIENT.ACTION_CANCEL", + className : "button", + callback : function cancelSubmission() { + $scope.client.requiredParameters = null; + $scope.disconnect(); + } + }; + + // Attempt to prompt for parameters only if the parameters that apply + // to the underlying connection are known + if (!$scope.client.protocol || !$scope.client.forms) + return; + + // Prompt for parameters + $scope.status = { + formNamespace : Protocol.getNamespace($scope.client.protocol), + forms : $scope.client.forms, + formModel : requiredParameters, + formSubmitCallback : SUBMIT_PARAMETERS.callback, + actions : [ SUBMIT_PARAMETERS, CANCEL_PARAMETER_SUBMISSION ] + }; + + }; + + /** + * Returns whether the given connection state allows for submission of + * connection parameters via "argv" instructions. + * + * @param {String} connectionState + * The connection state to test, as defined by + * ManagedClientState.ConnectionState. + * + * @returns {boolean} + * true if the given connection state allows submission of connection + * parameters via "argv" instructions, false otherwise. + */ + var canSubmitParameters = function canSubmitParameters(connectionState) { + return (connectionState === ManagedClientState.ConnectionState.WAITING || + connectionState === ManagedClientState.ConnectionState.CONNECTED); + }; + + // Show status dialog when connection status changes + $scope.$watchGroup([ + 'client.clientState.connectionState', + 'client.requiredParameters', + 'client.protocol', + 'client.forms' + ], function clientStateChanged(newValues) { + + var connectionState = newValues[0]; + var requiredParameters = newValues[1]; + + // Prompt for parameters only if parameters can actually be submitted + if (requiredParameters && canSubmitParameters(connectionState)) + notifyParametersRequired(requiredParameters); + + // Otherwise, just show general connection state + else + notifyConnectionState(connectionState); + + }); + + }]; + + return directive; + +}]); diff --git a/guacamole/src/main/frontend/src/app/client/styles/notification.css b/guacamole/src/main/frontend/src/app/client/styles/notification.css index 77a0a641a..5c8a8f989 100644 --- a/guacamole/src/main/frontend/src/app/client/styles/notification.css +++ b/guacamole/src/main/frontend/src/app/client/styles/notification.css @@ -17,7 +17,60 @@ * under the License. */ -.client .notification .parameters h3, -.client .notification .parameters .password-field .toggle-password { +.client-status-modal { + + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + + display: none; + background: rgba(0, 0, 0, 0.5); + +} + +.client-status-modal.shown { + display: block; +} + +.client-status-modal guac-modal { + position: absolute; +} + +.client-status-modal .notification { + background: rgba(80, 80, 80, 0.4); + color: white; + width: 100%; + max-width: 100%; + padding: 1em; + text-align: center; + border: none; +} + +.client-status-modal .notification .title-bar { + display: none +} + +.client-status-modal .notification .button { + background: transparent; + border: 2px solid white; + box-shadow: none; + text-shadow: none; + font-weight: normal; +} + +.client-status-modal .notification .button:hover { + text-decoration: underline; + background: rgba(255, 255, 255, 0.25); +} + +.client-status-modal .notification .button:active { + background: rgba(255, 255, 255, 0.5); +} + +.client-status-modal .notification .parameters h3, +.client-status-modal .notification .parameters .password-field .toggle-password { display: none; } + diff --git a/guacamole/src/main/frontend/src/app/client/templates/guacClientNotification.html b/guacamole/src/main/frontend/src/app/client/templates/guacClientNotification.html new file mode 100644 index 000000000..9948a2b1c --- /dev/null +++ b/guacamole/src/main/frontend/src/app/client/templates/guacClientNotification.html @@ -0,0 +1,5 @@ +
+ + + +
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 f1ceb64ac..ecae4efa3 100644 --- a/guacamole/src/main/frontend/src/app/client/templates/guacTiledClients.html +++ b/guacamole/src/main/frontend/src/app/client/templates/guacTiledClients.html @@ -8,6 +8,10 @@

{{ getClientTitle(client) }}

+ + + + \ No newline at end of file From bfd3cbc2049db07647582e36835488f3e60111dc Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Tue, 1 Jun 2021 17:39:50 -0700 Subject: [PATCH 04/58] GUACAMOLE-724: Do not reload client controller if only updating which client(s) are visible. Reloading the client controller would reset UI state, including whether the Guacamole menu is currently shown. --- .../client/controllers/clientController.js | 42 +++++++++++-------- .../src/app/index/config/indexRouteConfig.js | 1 + 2 files changed, 25 insertions(+), 18 deletions(-) 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 c19aa594e..e107cef5b 100644 --- a/guacamole/src/main/frontend/src/app/client/controllers/clientController.js +++ b/guacamole/src/main/frontend/src/app/client/controllers/clientController.js @@ -156,18 +156,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams * * @type ManagedClient[] */ - $scope.clients = (function getClients() { - - var clients = []; - - var ids = $routeParams.id.split(/[ +]/); - ids.forEach(function addClient(id) { - clients.push(guacClientManager.getManagedClient(id)); - }); - - return clients; - - })(); + $scope.clients = []; /** * All active clients which are not any current client ($scope.clients). @@ -175,17 +164,34 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams * * @type Object. */ - $scope.otherClients = (function getOtherClients(clients) { + $scope.otherClients = {}; - var otherClients = angular.extend({}, clients); + /** + * Reloads the contents of $scope.clients and $scope.otherClients to + * reflect the client IDs currently listed in the URL. + */ + var updateAttachedClients = function updateAttachedClients() { - $scope.clients.forEach(function removeActiveCLient(client) { - delete otherClients[client.id]; + var ids = $routeParams.id.split(/[ +]/); + + $scope.clients = []; + $scope.otherClients = angular.extend({}, guacClientManager.getManagedClients()); + + // 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]; }); - return otherClients; + }; - })(guacClientManager.getManagedClients()); + // Init sets of clients based on current URL ... + updateAttachedClients(); + + // ... and re-initialize those sets if the URL has changed without + // reloading the route + $scope.$on('$routeUpdate', updateAttachedClients); /** * The root connection groups of the connection hierarchy that should be diff --git a/guacamole/src/main/frontend/src/app/index/config/indexRouteConfig.js b/guacamole/src/main/frontend/src/app/index/config/indexRouteConfig.js index 684114514..322f7eaba 100644 --- a/guacamole/src/main/frontend/src/app/index/config/indexRouteConfig.js +++ b/guacamole/src/main/frontend/src/app/index/config/indexRouteConfig.js @@ -185,6 +185,7 @@ angular.module('index').config(['$routeProvider', '$locationProvider', bodyClassName : 'client', templateUrl : 'app/client/templates/client.html', controller : 'clientController', + reloadOnUrl : false, resolve : { updateCurrentToken: updateCurrentToken } }) From aae80292cb77ed298306c4fcdd646ff22b3ba603 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Wed, 16 Jun 2021 01:57:24 -0700 Subject: [PATCH 05/58] 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() }">
-