diff --git a/guacamole/src/main/webapp/app/client/controllers/clientController.js b/guacamole/src/main/webapp/app/client/controllers/clientController.js index 41c6ba6b7..5ee084b6e 100644 --- a/guacamole/src/main/webapp/app/client/controllers/clientController.js +++ b/guacamole/src/main/webapp/app/client/controllers/clientController.js @@ -24,22 +24,25 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams function clientController($scope, $routeParams, $injector) { // Required types + var ConnectionGroup = $injector.get('ConnectionGroup'); var ManagedClient = $injector.get('ManagedClient'); var ManagedClientState = $injector.get('ManagedClientState'); var ManagedFilesystem = $injector.get('ManagedFilesystem'); var ScrollState = $injector.get('ScrollState'); // Required services - var $location = $injector.get('$location'); - var authenticationService = $injector.get('authenticationService'); - var clipboardService = $injector.get('clipboardService'); - 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'); - var tunnelService = $injector.get('tunnelService'); - var userPageService = $injector.get('userPageService'); + 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'); + var tunnelService = $injector.get('tunnelService'); + var userPageService = $injector.get('userPageService'); /** * The minimum number of pixels a drag gesture must move to result in the @@ -264,6 +267,55 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams */ $scope.client = guacClientManager.getManagedClient($routeParams.id, $routeParams.params); + /** + * All active clients which are not the current client ($scope.client). + * 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]; + return otherClients; + })(guacClientManager.getManagedClients()); + + /** + * Map of data source identifier to the root connection group of that data + * source, or null if the connection group hierarchy has not yet been + * loaded. + * + * @type Object. + */ + $scope.rootConnectionGroups = null; + + /** + * Array of all connection properties that are filterable. + * + * @type String[] + */ + $scope.filteredConnectionProperties = [ + 'name' + ]; + + /** + * Array of all connection group properties that are filterable. + * + * @type String[] + */ + $scope.filteredConnectionGroupProperties = [ + 'name' + ]; + + // Retrieve root groups and all descendants + dataSourceService.apply( + connectionGroupService.getConnectionGroupTree, + authenticationService.getAvailableDataSources(), + ConnectionGroup.ROOT_IDENTIFIER + ) + .then(function rootGroupsRetrieved(rootConnectionGroups) { + $scope.rootConnectionGroups = rootConnectionGroups; + }, requestService.WARN); + /** * Map of all available sharing profiles for the current connection by * their identifiers. If this information is not yet available, or no such @@ -440,6 +492,12 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams }); + // Update last used timestamp when the active client changes + $scope.$watch('client', function clientChanged(client) { + if (client) + client.lastUsed = new Date().getTime(); + }); + // Update page icon when thumbnail changes $scope.$watch('client.thumbnail.canvas', function thumbnailChanged(canvas) { iconService.setIcons(canvas); diff --git a/guacamole/src/main/webapp/app/client/directives/guacClient.js b/guacamole/src/main/webapp/app/client/directives/guacClient.js index 769edd703..cc8829657 100644 --- a/guacamole/src/main/webapp/app/client/directives/guacClient.js +++ b/guacamole/src/main/webapp/app/client/directives/guacClient.js @@ -282,6 +282,9 @@ angular.module('client').directive('guacClient', [function guacClient() { return false; }; + // Size of newly-attached client may be different + $scope.mainElementResized(); + }); // Update actual view scrollLeft when scroll properties change diff --git a/guacamole/src/main/webapp/app/client/directives/guacClientPanel.js b/guacamole/src/main/webapp/app/client/directives/guacClientPanel.js new file mode 100644 index 000000000..658be2fdd --- /dev/null +++ b/guacamole/src/main/webapp/app/client/directives/guacClientPanel.js @@ -0,0 +1,170 @@ +/* + * 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 toolbar/panel which displays a list of active Guacamole connections. The + * panel is fixed to the bottom-right corner of its container and can be + * manually hidden/exposed by the user. + */ +angular.module('client').directive('guacClientPanel', ['$injector', function guacClientPanel($injector) { + + // Required services + var guacClientManager = $injector.get('guacClientManager'); + var sessionStorageFactory = $injector.get('sessionStorageFactory'); + + // Required types + var ManagedClientState = $injector.get('ManagedClientState'); + + /** + * Getter/setter for the boolean flag controlling whether the client panel + * is currently hidden. This flag is maintained in session-local storage to + * allow the state of the panel to persist despite navigation within the + * same tab. When hidden, the panel will be collapsed against the right + * side of the container. By default, the panel is visible. + * + * @type Function + */ + var panelHidden = sessionStorageFactory.create(false); + + return { + // Element only + restrict: 'E', + replace: true, + scope: { + + /** + * The ManagedClient instances associated with the active + * connections to be displayed within this panel. + * + * @type ManagedClient[]|Object. + */ + clients : '=' + + }, + templateUrl: 'app/client/templates/guacClientPanel.html', + controller: ['$scope', '$element', function guacClientPanelController($scope, $element) { + + /** + * The DOM element containing the scrollable portion of the client + * panel. + * + * @type Element + */ + var scrollableArea = $element.find('.client-panel-connection-list')[0]; + + /** + * On-scope reference to session-local storage of the flag + * controlling whether then panel is hidden. + */ + $scope.panelHidden = panelHidden; + + /** + * Returns whether this panel currently has any clients associated + * with it. + * + * @return {Boolean} + * true if at least one client is associated with this panel, + * false otherwise. + */ + $scope.hasClients = function hasClients() { + return !!_.find($scope.clients, $scope.isManaged); + }; + + /** + * 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. + * + * @param {ManagedClient} client + * The client to test. + * + * @returns {Boolean} + * true if the given client requires the user's attention, + * false otherwise. + */ + $scope.hasStatusUpdate = function hasStatusUpdate(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; + } + + return false; + + }; + + /** + * Returns whether the given client is currently being managed by + * the guacClientManager service. + * + * @param {ManagedClient} client + * The client to test. + * + * @returns {Boolean} + * true if the given client is being managed by the + * guacClientManager service, false otherwise. + */ + $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); + }; + + /** + * Toggles whether the client panel is currently hidden. + */ + $scope.togglePanel = function togglePanel() { + panelHidden(!panelHidden()); + }; + + // Override vertical scrolling, scrolling horizontally instead + scrollableArea.addEventListener('wheel', function reorientVerticalScroll(e) { + + var deltaMultiplier = { + /* DOM_DELTA_PIXEL */ 0x00: 1, + /* DOM_DELTA_LINE */ 0x01: 15, + /* DOM_DELTA_PAGE */ 0x02: scrollableArea.offsetWidth + }; + + if (e.deltaY) { + this.scrollLeft += e.deltaY * (deltaMultiplier[e.deltaMode] || deltaMultiplier(0x01)); + e.preventDefault(); + } + + }); + + }] + }; +}]); \ No newline at end of file diff --git a/guacamole/src/main/webapp/app/client/styles/guac-menu.css b/guacamole/src/main/webapp/app/client/styles/guac-menu.css index 9e0fb7ca7..48fb3b372 100644 --- a/guacamole/src/main/webapp/app/client/styles/guac-menu.css +++ b/guacamole/src/main/webapp/app/client/styles/guac-menu.css @@ -65,6 +65,29 @@ margin-top: 1em; } +#guac-menu .header h2 { + padding: 0; +} + +#guac-menu .header h2 .menu-dropdown { + border: none; +} + +#guac-menu .header h2 .menu-contents { + font-weight: normal; + font-size: 0.8em; +} + +#guac-menu .header .filter input { + border-bottom: 1px solid rgba(0,0,0,0.125); + border-left: none; +} + +#guac-menu .header .filter { + margin-bottom: 0.5em; + padding: 0; +} + #guac-menu #mouse-settings .choice { text-align: center; } diff --git a/guacamole/src/main/webapp/app/client/styles/keyboard.css b/guacamole/src/main/webapp/app/client/styles/keyboard.css index 8076d5480..e5bb96317 100644 --- a/guacamole/src/main/webapp/app/client/styles/keyboard.css +++ b/guacamole/src/main/webapp/app/client/styles/keyboard.css @@ -18,6 +18,8 @@ */ .keyboard-container { + + display: none; text-align: center; width: 100%; @@ -29,4 +31,9 @@ opacity: 0.85; z-index: 1; + } + +.keyboard-container.open { + display: block; +} \ No newline at end of file diff --git a/guacamole/src/main/webapp/app/client/styles/other-connections.css b/guacamole/src/main/webapp/app/client/styles/other-connections.css new file mode 100644 index 000000000..6c57aaaa8 --- /dev/null +++ b/guacamole/src/main/webapp/app/client/styles/other-connections.css @@ -0,0 +1,206 @@ +/* + * 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. + */ + +#other-connections .client-panel { + + display: none; + position: absolute; + right: 0; + bottom: 0; + + border: 1px solid rgba(255, 255, 255, 0.25); + background: rgba(0, 0, 0, 0.25); + max-width: 100%; + white-space: nowrap; + transition: max-width 0.125s, width 0.125s; + + /* Render above modal status */ + z-index: 20; + +} + +#other-connections .client-panel.has-clients { + display: block; +} + +#other-connections .client-panel.hidden { + max-width: 16px; +} + +#other-connections .client-panel-handle { + + position: absolute; + left: 0; + bottom: 0; + height: 100%; + width: 16px; + z-index: 1; + + background-color: white; + background-repeat: no-repeat; + background-size: contain; + background-position: center center; + background-image: url(images/arrows/right.png); + opacity: 0.5; + +} + +#other-connections .client-panel-handle:hover { + opacity: 0.75; +} + +#other-connections .client-panel.hidden .client-panel-handle { + background-image: url(images/arrows/left.png); +} + +#other-connections .client-panel-connection-list { + + text-align: right; + + margin: 0; + padding: 0; + padding-left: 16px; + + overflow-x: auto; + overflow-y: hidden; + +} + +#other-connections .client-panel-connection { + + display: inline-block; + position: relative; + + margin: 0.5em; + border: 1px solid white; + background: black; + box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.5); + + opacity: 0.5; + transition: opacity 0.25s; + + max-height: 128px; + overflow: hidden; + vertical-align: middle; + +} + +#other-connections .client-panel-connection .thumbnail-main img { + max-width: none; + max-height: 128px; +} + +#other-connections .client-panel-connection a[href]::before { + + display: block; + content: ' '; + + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 100%; + z-index: 1; + + background: url('images/warning-white.png'); + background-size: 48px; + background-position: center; + background-repeat: no-repeat; + background-color: black; + + opacity: 0; + transition: opacity 0.25s; + +} + +#other-connections .client-panel-connection.needs-attention a[href]::before { + opacity: 0.75; +} + +#other-connections button.close-other-connection { + + position: absolute; + top: 0; + right: 0; + z-index: 2; + + margin: 0; + padding: 4px; + min-width: 0; + border: none; + background: transparent; + box-shadow: none; + text-shadow: none; + + opacity: 0.5; + line-height: 1; + +} + +#other-connections button.close-other-connection:hover { + opacity: 1; +} + +#other-connections button.close-other-connection img { + background: #A43; + border-radius: 18px; + max-width: 18px; + padding: 3px; +} + +#other-connections button.close-other-connection:hover img { + background: #C54; +} + +#other-connections .client-panel.hidden .client-panel-connection-list { + /* Hide scrollbar when panel is hidden (will be visible through panel + * show/hide button otherwise) */ + overflow-x: hidden; +} + +#other-connections .client-panel.hidden .client-panel-connection { + /* Hide thumbnails when panel is hidden (will be visible through panel + * show/hide button otherwise) */ + visibility: hidden; +} + +#other-connections .client-panel-connection .name { + + position: absolute; + padding: 0.25em 0.5em; + left: 0; + right: 0; + bottom: 0; + z-index: 2; + + text-align: left; + color: white; + background: rgba(0, 0, 0, 0.5); + font-size: 0.75em; + font-weight: bold; + + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + +} + +#other-connections .client-panel-connection:hover { + opacity: 1; +} diff --git a/guacamole/src/main/webapp/app/client/styles/text-input.css b/guacamole/src/main/webapp/app/client/styles/text-input.css new file mode 100644 index 000000000..28e905ef4 --- /dev/null +++ b/guacamole/src/main/webapp/app/client/styles/text-input.css @@ -0,0 +1,26 @@ +/* + * 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. + */ + +.text-input-container { + display: none; +} + +.text-input-container.open { + display: block; +} diff --git a/guacamole/src/main/webapp/app/client/templates/client.html b/guacamole/src/main/webapp/app/client/templates/client.html index ad85f234e..9d06549f9 100644 --- a/guacamole/src/main/webapp/app/client/templates/client.html +++ b/guacamole/src/main/webapp/app/client/templates/client.html @@ -8,21 +8,26 @@
- + + +
+ +
+
-
+
-
+
@@ -47,7 +52,25 @@
-

{{client.name}}

+

{{client.name}}

+

+ +
+ + +
+
+