diff --git a/guacamole/src/main/webapp/app/groupList/directives/guacGroupList.js b/guacamole/src/main/webapp/app/groupList/directives/guacGroupList.js index d5404c778..87adb4947 100644 --- a/guacamole/src/main/webapp/app/groupList/directives/guacGroupList.js +++ b/guacamole/src/main/webapp/app/groupList/directives/guacGroupList.js @@ -46,37 +46,18 @@ angular.module('groupList').directive('guacGroupList', [function guacGroupList() context : '=', /** - * The URL or ID of the Angular template to use when rendering a - * connection. The @link{GroupListItem} associated with that - * connection will be exposed within the scope of the template - * as item, and the arbitrary context object, if any, - * will be exposed as context. + * The map of @link{GroupListItem} type to the URL or ID of the + * Angular template to use when rendering a @link{GroupListItem} of + * that type. The @link{GroupListItem} itself will be within the + * scope of the template as item, and the arbitrary + * context object, if any, will be exposed as context. + * If the template for a type is omitted, items of that type will + * not be rendered. All standard types are defined by + * @link{GroupListItem.Type}, but use of custom types is legal. * - * @type String + * @type Object. */ - connectionTemplate : '=', - - /** - * The URL or ID of the Angular template to use when rendering a - * connection group. The @link{GroupListItem} associated with that - * connection group will be exposed within the scope of the - * template as item, and the arbitrary context object, - * if any, will be exposed as context. - * - * @type String - */ - connectionGroupTemplate : '=', - - /** - * The URL or ID of the Angular template to use when rendering a - * sharing profile. The @link{GroupListItem} associated with that - * sharing profile will be exposed within the scope of the template - * as item, and the arbitrary context object, if any, - * will be exposed as context. - * - * @type String - */ - sharingProfileTemplate : '=', + templates : '=', /** * Whether the root of the connection group hierarchy given should @@ -92,7 +73,18 @@ angular.module('groupList').directive('guacGroupList', [function guacGroupList() * * @type Number */ - pageSize : '=' + pageSize : '=', + + /** + * A callback which accepts an array of GroupListItems as its sole + * parameter. If provided, the callback will be invoked whenever an + * array of root-level GroupListItems is about to be rendered. + * Changes may be made by this function to that array or to the + * GroupListItems themselves. + * + * @type Function + */ + decorator : '=' }, @@ -145,51 +137,20 @@ angular.module('groupList').directive('guacGroupList', [function guacGroupList() }; /** - * Returns whether the given item represents a connection that can - * be displayed. If there is no connection template, then no - * connection is visible. - * - * @param {GroupListItem} item - * The item to check. + * Returns whether a @link{GroupListItem} of the given type can be + * displayed. If there is no template associated with the given + * type, then a @link{GroupListItem} of that type cannot be + * displayed. + * + * @param {String} type + * The type to check. * * @returns {Boolean} - * true if the given item is a connection that can be - * displayed, false otherwise. + * true if the given @link{GroupListItem} type can be displayed, + * false otherwise. */ - $scope.isVisibleConnection = function isVisibleConnection(item) { - return item.isConnection && !!$scope.connectionTemplate; - }; - - /** - * Returns whether the given item represents a connection group - * that can be displayed. If there is no connection group template, - * then no connection group is visible. - * - * @param {GroupListItem} item - * The item to check. - * - * @returns {Boolean} - * true if the given item is a connection group that can be - * displayed, false otherwise. - */ - $scope.isVisibleConnectionGroup = function isVisibleConnectionGroup(item) { - return item.isConnectionGroup && !!$scope.connectionGroupTemplate; - }; - - /** - * Returns whether the given item represents a sharing profile that - * can be displayed. If there is no sharing profile template, then - * no sharing profile is visible. - * - * @param {GroupListItem} item - * The item to check. - * - * @returns {Boolean} - * true if the given item is a sharing profile that can be - * displayed, false otherwise. - */ - $scope.isVisibleSharingProfile = function isVisibleSharingProfile(item) { - return item.isSharingProfile && !!$scope.sharingProfileTemplate; + $scope.isVisible = function isVisible(type) { + return !!$scope.templates[type]; }; // Set contents whenever the connection group is assigned or changed @@ -212,7 +173,8 @@ angular.module('groupList').directive('guacGroupList', [function guacGroupList() // Create root item for current connection group var rootItem = GroupListItem.fromConnectionGroup(dataSource, connectionGroup, - !!$scope.connectionTemplate, !!$scope.sharingProfileTemplate, + $scope.isVisible(GroupListItem.Type.CONNECTION), + $scope.isVisible(GroupListItem.Type.SHARING_PROFILE), countActiveConnections); // If root group is to be shown, add it as a root item @@ -255,6 +217,10 @@ angular.module('groupList').directive('guacGroupList', [function guacGroupList() } + // Invoke item decorator, if provided + if ($scope.decorator) + $scope.decorator($scope.rootItems); + }); /** @@ -265,7 +231,7 @@ angular.module('groupList').directive('guacGroupList', [function guacGroupList() * connection group. */ $scope.toggleExpanded = function toggleExpanded(groupListItem) { - groupListItem.isExpanded = !groupListItem.isExpanded; + groupListItem.expanded = !groupListItem.expanded; }; }] diff --git a/guacamole/src/main/webapp/app/groupList/templates/guacGroupList.html b/guacamole/src/main/webapp/app/groupList/templates/guacGroupList.html index df9b5b6c1..4c9bb424c 100644 --- a/guacamole/src/main/webapp/app/groupList/templates/guacGroupList.html +++ b/guacamole/src/main/webapp/app/groupList/templates/guacGroupList.html @@ -1,55 +1,31 @@
@@ -58,7 +34,7 @@
- diff --git a/guacamole/src/main/webapp/app/groupList/types/GroupListItem.js b/guacamole/src/main/webapp/app/groupList/types/GroupListItem.js index 2fc9cf5b0..3591dd0ce 100644 --- a/guacamole/src/main/webapp/app/groupList/types/GroupListItem.js +++ b/guacamole/src/main/webapp/app/groupList/types/GroupListItem.js @@ -77,43 +77,36 @@ angular.module('groupList').factory('GroupListItem', ['ConnectionGroup', functio this.children = template.children || []; /** - * Whether this item represents a connection. If this item represents - * a connection group or sharing profile, this MUST be false. + * The type of object represented by this GroupListItem. Standard types + * are defined by GroupListItem.Type, but custom types are also legal. * - * @type Boolean + * @type String */ - this.isConnection = template.isConnection; + this.type = template.type; /** - * Whether this item represents a connection group. If this item - * represents a connection or sharing profile, this MUST be false. + * Whether this item, or items of the same type, can contain children. + * This may be true even if this particular item does not presently + * contain children. * * @type Boolean */ - this.isConnectionGroup = template.isConnectionGroup; - - /** - * Whether this item represents a sharing profile. If this item - * represents a connection or connection group, this MUST be false. - * - * @type Boolean - */ - this.isSharingProfile = template.isSharingProfile; + this.expandable = template.expandable; /** * Whether this item represents a balancing connection group. * * @type Boolean */ - this.isBalancing = template.isBalancing; + this.balancing = template.balancing; /** * Whether the children items should be displayed. * * @type Boolean */ - this.isExpanded = template.isExpanded; - + this.expanded = template.expanded; + /** * Returns the number of currently active users for this connection, * connection group, or sharing profile, if known. @@ -126,12 +119,24 @@ angular.module('groupList').factory('GroupListItem', ['ConnectionGroup', functio /** * The connection, connection group, or sharing profile whose data is - * exposed within this GroupListItem. + * exposed within this GroupListItem. If the type of this GroupListItem + * is not one of the types defined by GroupListItem.Type, then this + * value may be anything. * - * @type Connection|ConnectionGroup|SharingProfile + * @type Connection|ConnectionGroup|SharingProfile|* */ this.wrappedItem = template.wrappedItem; + /** + * The sorting weight to apply when displaying this GroupListItem. This + * weight is relative only to other sorting weights. If two items have + * the same weight, they will be sorted based on their names. + * + * @type Number + * @default 0 + */ + this.weight = template.weight || 0; + }; /** @@ -182,9 +187,8 @@ angular.module('groupList').factory('GroupListItem', ['ConnectionGroup', functio dataSource : dataSource, // Type information - isConnection : true, - isConnectionGroup : false, - isSharingProfile : false, + expandable : includeSharingProfiles, + type : GroupListItem.Type.CONNECTION, // Already-converted children children : children, @@ -277,10 +281,9 @@ angular.module('groupList').factory('GroupListItem', ['ConnectionGroup', functio dataSource : dataSource, // Type information - isConnection : false, - isConnectionGroup : true, - isSharingProfile : false, - isBalancing : connectionGroup.type === ConnectionGroup.Type.BALANCING, + type : GroupListItem.Type.CONNECTION_GROUP, + balancing : connectionGroup.type === ConnectionGroup.Type.BALANCING, + expandable : true, // Already-converted children children : children, @@ -331,9 +334,7 @@ angular.module('groupList').factory('GroupListItem', ['ConnectionGroup', functio dataSource : dataSource, // Type information - isConnection : false, - isConnectionGroup : false, - isSharingProfile : true, + type : GroupListItem.Type.SHARING_PROFILE, // Wrapped item wrappedItem : sharingProfile @@ -342,6 +343,42 @@ angular.module('groupList').factory('GroupListItem', ['ConnectionGroup', functio }; + /** + * All pre-defined types of GroupListItems. Note that, while these are the + * standard types supported by GroupListItem and the related guacGroupList + * directive, the type string is otherwise arbitrary and custom types are + * legal. + * + * @type Object. + */ + GroupListItem.Type = { + + /** + * The standard type string of a GroupListItem which represents a + * connection. + * + * @type String + */ + CONNECTION : 'connection', + + /** + * The standard type string of a GroupListItem which represents a + * connection group. + * + * @type String + */ + CONNECTION_GROUP : 'connection-group', + + /** + * The standard type string of a GroupListItem which represents a + * sharing profile. + * + * @type String + */ + SHARING_PROFILE : 'sharing-profile' + + }; + return GroupListItem; }]); diff --git a/guacamole/src/main/webapp/app/home/controllers/homeController.js b/guacamole/src/main/webapp/app/home/controllers/homeController.js index aff81ae83..150ac4ead 100644 --- a/guacamole/src/main/webapp/app/home/controllers/homeController.js +++ b/guacamole/src/main/webapp/app/home/controllers/homeController.js @@ -26,6 +26,7 @@ angular.module('home').controller('homeController', ['$scope', '$injector', // Get required types var ConnectionGroup = $injector.get('ConnectionGroup'); var ClientIdentifier = $injector.get('ClientIdentifier'); + var GroupListItem = $injector.get('GroupListItem'); // Get required services var authenticationService = $injector.get('authenticationService'); @@ -95,15 +96,15 @@ angular.module('home').controller('homeController', ['$scope', '$injector', getClientIdentifier : function getClientIdentifier(item) { // If the item is a connection, generate a connection identifier - if (item.isConnection) + if (item.type === GroupListItem.Type.CONNECTION) return ClientIdentifier.toString({ dataSource : item.dataSource, type : ClientIdentifier.Types.CONNECTION, id : item.identifier }); - // If the item is a connection, generate a connection group identifier - if (item.isConnectionGroup) + // If the item is a connection group, generate a connection group identifier + if (item.type === GroupListItem.Type.CONNECTION_GROUP) return ClientIdentifier.toString({ dataSource : item.dataSource, type : ClientIdentifier.Types.CONNECTION_GROUP, diff --git a/guacamole/src/main/webapp/app/home/templates/connectionGroup.html b/guacamole/src/main/webapp/app/home/templates/connectionGroup.html index 1fe151570..7356883c2 100644 --- a/guacamole/src/main/webapp/app/home/templates/connectionGroup.html +++ b/guacamole/src/main/webapp/app/home/templates/connectionGroup.html @@ -1,5 +1,4 @@ - - {{item.name}} - {{item.name}} + {{item.name}} + {{item.name}} diff --git a/guacamole/src/main/webapp/app/home/templates/home.html b/guacamole/src/main/webapp/app/home/templates/home.html index 727194520..f68a0a9f9 100644 --- a/guacamole/src/main/webapp/app/home/templates/home.html +++ b/guacamole/src/main/webapp/app/home/templates/home.html @@ -25,8 +25,10 @@ diff --git a/guacamole/src/main/webapp/app/index/styles/lists.css b/guacamole/src/main/webapp/app/index/styles/lists.css index 5ce162a13..0c761aef3 100644 --- a/guacamole/src/main/webapp/app/index/styles/lists.css +++ b/guacamole/src/main/webapp/app/index/styles/lists.css @@ -18,28 +18,28 @@ */ .user, -.group, +.connection-group, .connection { cursor: pointer; } .user a, .connection a, -.group a { +.connection-group a { text-decoration:none; color: black; } .user a:hover, .connection a:hover, -.group a:hover { +.connection-group a:hover { text-decoration:none; color: black; } .user a:visited, .connection a:visited, -.group a:visited { +.connection-group a:visited { text-decoration:none; color: black; } diff --git a/guacamole/src/main/webapp/app/index/styles/ui.css b/guacamole/src/main/webapp/app/index/styles/ui.css index 62db41fb5..434f443f6 100644 --- a/guacamole/src/main/webapp/app/index/styles/ui.css +++ b/guacamole/src/main/webapp/app/index/styles/ui.css @@ -178,11 +178,11 @@ div.section { background-position: center center; } -.group > .caption .icon { +.connection-group > .caption .icon { background-image: url('images/folder-closed.png'); } -.group.expanded > .caption .icon { +.connection-group.expanded > .caption .icon { background-image: url('images/folder-open.png'); } @@ -213,7 +213,7 @@ div.section { padding-left: 13px; } -.group.empty.balancer .icon { +.connection-group.empty.balancer .icon { background-image: url('images/protocol-icons/guac-monitor.png'); } diff --git a/guacamole/src/main/webapp/app/manage/controllers/manageConnectionController.js b/guacamole/src/main/webapp/app/manage/controllers/manageConnectionController.js index 0e21873dd..a911ab2ec 100644 --- a/guacamole/src/main/webapp/app/manage/controllers/manageConnectionController.js +++ b/guacamole/src/main/webapp/app/manage/controllers/manageConnectionController.js @@ -291,7 +291,10 @@ angular.module('manage').controller('manageConnectionController', ['$scope', '$i // If we are creating a new connection, populate skeleton connection data else { - $scope.connection = new Connection({ protocol: 'vnc' }); + $scope.connection = new Connection({ + protocol : 'vnc', + parentIdentifier : $location.search().parent + }); $scope.historyEntryWrappers = []; $scope.parameters = {}; } diff --git a/guacamole/src/main/webapp/app/manage/controllers/manageConnectionGroupController.js b/guacamole/src/main/webapp/app/manage/controllers/manageConnectionGroupController.js index d76b27a90..0d0af5921 100644 --- a/guacamole/src/main/webapp/app/manage/controllers/manageConnectionGroupController.js +++ b/guacamole/src/main/webapp/app/manage/controllers/manageConnectionGroupController.js @@ -175,7 +175,9 @@ angular.module('manage').controller('manageConnectionGroupController', ['$scope' // If we are creating a new connection group, populate skeleton connection group data else - $scope.connectionGroup = new ConnectionGroup(); + $scope.connectionGroup = new ConnectionGroup({ + parentIdentifier : $location.search().parent + }); /** * Available connection group types, as translation string / internal value diff --git a/guacamole/src/main/webapp/app/manage/templates/locationChooser.html b/guacamole/src/main/webapp/app/manage/templates/locationChooser.html index 57c957e5a..baeebded9 100644 --- a/guacamole/src/main/webapp/app/manage/templates/locationChooser.html +++ b/guacamole/src/main/webapp/app/manage/templates/locationChooser.html @@ -9,7 +9,9 @@ context="groupListContext" show-root-group="true" connection-groups="rootGroups" - connection-group-template="'app/manage/templates/locationChooserConnectionGroup.html'"/> + templates="{ + 'connection-group' : 'app/manage/templates/locationChooserConnectionGroup.html' + }"/> diff --git a/guacamole/src/main/webapp/app/manage/templates/manageUser.html b/guacamole/src/main/webapp/app/manage/templates/manageUser.html index 82bc1e978..e32d725ae 100644 --- a/guacamole/src/main/webapp/app/manage/templates/manageUser.html +++ b/guacamole/src/main/webapp/app/manage/templates/manageUser.html @@ -78,9 +78,11 @@ diff --git a/guacamole/src/main/webapp/app/settings/directives/guacSettingsConnections.js b/guacamole/src/main/webapp/app/settings/directives/guacSettingsConnections.js index 1414f9107..b174a8da0 100644 --- a/guacamole/src/main/webapp/app/settings/directives/guacSettingsConnections.js +++ b/guacamole/src/main/webapp/app/settings/directives/guacSettingsConnections.js @@ -35,6 +35,7 @@ angular.module('settings').directive('guacSettingsConnections', [function guacSe // Required types var ConnectionGroup = $injector.get('ConnectionGroup'); + var GroupListItem = $injector.get('GroupListItem'); var PermissionSet = $injector.get('PermissionSet'); // Required services @@ -205,6 +206,112 @@ angular.module('settings').directive('guacSettingsConnections', [function guacSe }; + /** + * Returns whether the current user can update the connection group + * having the given identifier within the current data source. + * + * @param {String} identifier + * The identifier of the connection group to check. + * + * @return {Boolean} + * true if the current user can update the connection group + * having the given identifier within the current data source, + * false otherwise. + */ + $scope.canUpdateConnectionGroup = function canUpdateConnectionGroup(identifier) { + + // Abort if permissions have not yet loaded + if (!$scope.permissions) + return false; + + // Can update the connection if adminstrator or have explicit permission + if (PermissionSet.hasSystemPermission($scope.permissions, PermissionSet.SystemPermissionType.ADMINISTER) + || PermissionSet.hasConnectionGroupPermission($scope.permissions, PermissionSet.ObjectPermissionType.UPDATE, identifier)) + return true; + + // Current data sources does not allow the connection group to be updated + return false; + + }; + + /** + * Adds connection-group-specific contextual actions to the given + * array of GroupListItems. Each contextual action will be + * represented by a new GroupListItem. + * + * @param {GroupListItem[]} items + * The array of GroupListItems to which new GroupListItems + * representing connection-group-specific contextual actions + * should be added. + * + * @param {GroupListItem} [parent] + * The GroupListItem representing the connection group which + * contains the given array of GroupListItems, if known. + */ + var addConnectionGroupActions = function addConnectionGroupActions(items, parent) { + + // Do nothing if we lack permission to modify the parent at all + if (parent && !$scope.canUpdateConnectionGroup(parent.identifier)) + return; + + // Add action for creating a child connection, if the user has + // permission to do so + if ($scope.canCreateConnections()) + items.push(new GroupListItem({ + type : 'new-connection', + dataSource : $scope.dataSource, + weight : 1, + wrappedItem : parent + })); + + // Add action for creating a child connection group, if the user + // has permission to do so + if ($scope.canCreateConnectionGroups()) + items.push(new GroupListItem({ + type : 'new-connection-group', + dataSource : $scope.dataSource, + weight : 1, + wrappedItem : parent + })); + + }; + + /** + * Decorates the given GroupListItem, including all descendants, + * adding contextual actions. + * + * @param {GroupListItem} item + * The GroupListItem which should be decorated with additional + * GroupListItems representing contextual actions. + */ + var decorateItem = function decorateItem(item) { + + // If the item is a connection group, add actions specific to + // connection groups + if (item.type === GroupListItem.Type.CONNECTION_GROUP) + addConnectionGroupActions(item.children, item); + + // Decorate all children + angular.forEach(item.children, decorateItem); + + }; + + /** + * Callback which decorates all items within the given array of + * GroupListItems, including their descendants, adding contextual + * actions. + * + * @param {GroupListItem[]} items + * The array of GroupListItems which should be decorated with + * additional GroupListItems representing contextual actions. + */ + $scope.rootItemDecorator = function rootItemDecorator(items) { + + // Decorate each root-level item + angular.forEach(items, decorateItem); + + }; + // Retrieve current permissions permissionService.getPermissions($scope.dataSource, currentUsername) .success(function permissionsRetrieved(permissions) { @@ -219,19 +326,19 @@ angular.module('settings').directive('guacSettingsConnections', [function guacSe if (!$scope.canManageConnections()) $location.path('/'); - }); - - // Retrieve all connections for which we have UPDATE or DELETE permission - dataSourceService.apply( - connectionGroupService.getConnectionGroupTree, - [$scope.dataSource], - ConnectionGroup.ROOT_IDENTIFIER, - [PermissionSet.ObjectPermissionType.UPDATE, PermissionSet.ObjectPermissionType.DELETE] - ) - .then(function connectionGroupsReceived(rootGroups) { - $scope.rootGroups = rootGroups; - }); - + // Retrieve all connections for which we have UPDATE or DELETE permission + dataSourceService.apply( + connectionGroupService.getConnectionGroupTree, + [$scope.dataSource], + ConnectionGroup.ROOT_IDENTIFIER, + [PermissionSet.ObjectPermissionType.UPDATE, PermissionSet.ObjectPermissionType.DELETE] + ) + .then(function connectionGroupsReceived(rootGroups) { + $scope.rootGroups = rootGroups; + }); + + }); // end retrieve permissions + }] }; diff --git a/guacamole/src/main/webapp/app/settings/styles/connection-list.css b/guacamole/src/main/webapp/app/settings/styles/connection-list.css new file mode 100644 index 000000000..7bf35538a --- /dev/null +++ b/guacamole/src/main/webapp/app/settings/styles/connection-list.css @@ -0,0 +1,34 @@ +/* + * 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. + */ + +.settings.connections .connection-list .new-connection, +.settings.connections .connection-list .new-connection-group { + opacity: 0.5; + font-style: italic; +} + +.settings.connections .connection-list .new-connection a, +.settings.connections .connection-list .new-connection a:hover, +.settings.connections .connection-list .new-connection a:visited, +.settings.connections .connection-list .new-connection-group a, +.settings.connections .connection-list .new-connection-group a:hover, +.settings.connections .connection-list .new-connection-group a:visited { + text-decoration:none; + color: black; +} diff --git a/guacamole/src/main/webapp/app/settings/templates/newConnection.html b/guacamole/src/main/webapp/app/settings/templates/newConnection.html new file mode 100644 index 000000000..e4debbe0d --- /dev/null +++ b/guacamole/src/main/webapp/app/settings/templates/newConnection.html @@ -0,0 +1,3 @@ + + {{'SETTINGS_CONNECTIONS.ACTION_NEW_CONNECTION' | translate}} + diff --git a/guacamole/src/main/webapp/app/settings/templates/newConnectionGroup.html b/guacamole/src/main/webapp/app/settings/templates/newConnectionGroup.html new file mode 100644 index 000000000..c1d6bba5f --- /dev/null +++ b/guacamole/src/main/webapp/app/settings/templates/newConnectionGroup.html @@ -0,0 +1,3 @@ + + {{'SETTINGS_CONNECTIONS.ACTION_NEW_CONNECTION_GROUP' | translate}} + diff --git a/guacamole/src/main/webapp/app/settings/templates/settingsConnections.html b/guacamole/src/main/webapp/app/settings/templates/settingsConnections.html index ea1805bb7..dfdf686ef 100644 --- a/guacamole/src/main/webapp/app/settings/templates/settingsConnections.html +++ b/guacamole/src/main/webapp/app/settings/templates/settingsConnections.html @@ -33,7 +33,15 @@ + decorator="rootItemDecorator" + templates="{ + + 'connection' : 'app/settings/templates/connection.html', + 'connection-group' : 'app/settings/templates/connectionGroup.html', + + 'new-connection' : 'app/settings/templates/newConnection.html', + 'new-connection-group' : 'app/settings/templates/newConnectionGroup.html' + + }"/>