From db02d9a7fbfb0fcfbc69c8a1d67edcf64da6d7bd Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Sun, 7 Aug 2016 23:50:41 -0700 Subject: [PATCH] GUACAMOLE-5: Implement management of sharing profiles. --- .../app/index/config/indexRouteConfig.js | 9 + .../manageSharingProfileController.js | 404 ++++++++++++++++++ .../templates/manageSharingProfile.html | 44 ++ .../src/main/webapp/translations/de.json | 16 + .../src/main/webapp/translations/en.json | 21 + .../src/main/webapp/translations/fr.json | 16 + .../src/main/webapp/translations/it.json | 16 + .../src/main/webapp/translations/nl.json | 16 + .../src/main/webapp/translations/ru.json | 16 + 9 files changed, 558 insertions(+) create mode 100644 guacamole/src/main/webapp/app/manage/controllers/manageSharingProfileController.js create mode 100644 guacamole/src/main/webapp/app/manage/templates/manageSharingProfile.html diff --git a/guacamole/src/main/webapp/app/index/config/indexRouteConfig.js b/guacamole/src/main/webapp/app/index/config/indexRouteConfig.js index 0eb9fbc54..904ab85b4 100644 --- a/guacamole/src/main/webapp/app/index/config/indexRouteConfig.js +++ b/guacamole/src/main/webapp/app/index/config/indexRouteConfig.js @@ -137,6 +137,15 @@ angular.module('index').config(['$routeProvider', '$locationProvider', resolve : { updateCurrentToken: updateCurrentToken } }) + // Sharing profile editor + .when('/manage/:dataSource/sharingProfiles/:id?', { + title : 'APP.NAME', + bodyClassName : 'manage', + templateUrl : 'app/manage/templates/manageSharingProfile.html', + controller : 'manageSharingProfileController', + resolve : { updateCurrentToken: updateCurrentToken } + }) + // Connection group editor .when('/manage/:dataSource/connectionGroups/:id?', { title : 'APP.NAME', diff --git a/guacamole/src/main/webapp/app/manage/controllers/manageSharingProfileController.js b/guacamole/src/main/webapp/app/manage/controllers/manageSharingProfileController.js new file mode 100644 index 000000000..96657d173 --- /dev/null +++ b/guacamole/src/main/webapp/app/manage/controllers/manageSharingProfileController.js @@ -0,0 +1,404 @@ +/* + * 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. + */ + +/** + * The controller for editing or creating sharing profiles. + */ +angular.module('manage').controller('manageSharingProfileController', ['$scope', '$injector', + function manageSharingProfileController($scope, $injector) { + + // Required types + var SharingProfile = $injector.get('SharingProfile'); + var PermissionSet = $injector.get('PermissionSet'); + + // Required services + var $location = $injector.get('$location'); + var $routeParams = $injector.get('$routeParams'); + var authenticationService = $injector.get('authenticationService'); + var connectionService = $injector.get('connectionService'); + var guacNotification = $injector.get('guacNotification'); + var permissionService = $injector.get('permissionService'); + var schemaService = $injector.get('schemaService'); + var sharingProfileService = $injector.get('sharingProfileService'); + var translationStringService = $injector.get('translationStringService'); + + /** + * An action which can be provided along with the object sent to showStatus + * to allow the user to acknowledge (and close) the currently-shown status + * dialog. + */ + var ACKNOWLEDGE_ACTION = { + name : "MANAGE_SHARING_PROFILE.ACTION_ACKNOWLEDGE", + callback : function acknowledgeCallback() { + guacNotification.showStatus(false); + } + }; + + /** + * An action to be provided along with the object sent to showStatus which + * closes the currently-shown status dialog, effectively canceling the + * operation which was pending user confirmation. + */ + var CANCEL_ACTION = { + name : "MANAGE_SHARING_PROFILE.ACTION_CANCEL", + callback : function cancelCallback() { + guacNotification.showStatus(false); + } + }; + + /** + * The unique identifier of the data source containing the sharing profile + * being edited. + * + * @type String + */ + $scope.selectedDataSource = $routeParams.dataSource; + + /** + * The identifier of the original sharing profile from which this sharing + * profile is being cloned. Only valid if this is a new sharing profile. + * + * @type String + */ + var cloneSourceIdentifier = $location.search().clone; + + /** + * The identifier of the sharing profile being edited. If a new sharing + * profile is being created, this will not be defined. + * + * @type String + */ + var identifier = $routeParams.id; + + /** + * Map of protocol name to corresponding Protocol object. + * + * @type Object. + */ + $scope.protocols = null; + + /** + * The sharing profile being modified. + * + * @type SharingProfile + */ + $scope.sharingProfile = null; + + /** + * The parameter name/value pairs associated with the sharing profile being + * modified. + * + * @type Object. + */ + $scope.parameters = null; + + /** + * Whether the user can save the sharing profile being edited. This could be + * updating an existing sharing profile, or creating a new sharing profile. + * + * @type Boolean + */ + $scope.canSaveSharingProfile = null; + + /** + * Whether the user can delete the sharing profile being edited. + * + * @type Boolean + */ + $scope.canDeleteSharingProfile = null; + + /** + * Whether the user can clone the sharing profile being edited. + * + * @type Boolean + */ + $scope.canCloneSharingProfile = null; + + /** + * All permissions associated with the current user, or null if the user's + * permissions have not yet been loaded. + * + * @type PermissionSet + */ + $scope.permissions = null; + + /** + * All available sharing profile attributes. This is only the set of + * attribute definitions, organized as logical groupings of attributes, not + * attribute values. + * + * @type Form[] + */ + $scope.attributes = null; + + /** + * Returns whether critical data has completed being loaded. + * + * @returns {Boolean} + * true if enough data has been loaded for the user interface to be + * useful, false otherwise. + */ + $scope.isLoaded = function isLoaded() { + + return $scope.protocols !== null + && $scope.sharingProfile !== null + && $scope.primaryConnection !== null + && $scope.parameters !== null + && $scope.permissions !== null + && $scope.attributes !== null + && $scope.canSaveSharingProfile !== null + && $scope.canDeleteSharingProfile !== null + && $scope.canCloneSharingProfile !== null; + + }; + + // Pull sharing profile attribute schema + schemaService.getSharingProfileAttributes($scope.selectedDataSource) + .success(function attributesReceived(attributes) { + $scope.attributes = attributes; + }); + + // Query the user's permissions for the current sharing profile + permissionService.getPermissions($scope.selectedDataSource, authenticationService.getCurrentUsername()) + .success(function permissionsReceived(permissions) { + + $scope.permissions = permissions; + + // The sharing profile can be saved if it is new or if the user has + // UPDATE permission for that sharing profile (either explicitly or by + // virtue of being an administrator) + $scope.canSaveSharingProfile = + !identifier + || PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.ADMINISTER) + || PermissionSet.hasSharingProfilePermission(permissions, PermissionSet.ObjectPermissionType.UPDATE, identifier); + + // The sharing profile can be saved only if it exists and the user has + // DELETE permission (either explicitly or by virtue of being an + // administrator) + $scope.canDeleteSharingProfile = + !!identifier && ( + PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.ADMINISTER) + || PermissionSet.hasSharingProfilePermission(permissions, PermissionSet.ObjectPermissionType.DELETE, identifier) + ); + + // The sharing profile can be cloned only if it exists, the user has + // UPDATE permission on the sharing profile being cloned (is able to + // read parameters), and the user can create new sharing profiles + $scope.canCloneSharingProfile = + !!identifier && ( + PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.ADMINISTER) || ( + PermissionSet.hasSharingProfilePermission(permissions, PermissionSet.ObjectPermissionType.UPDATE, identifier) + && PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.CREATE_SHARING_PROFILE) + ) + ); + + }); + + // Get protocol metadata + schemaService.getProtocols($scope.selectedDataSource) + .success(function protocolsReceived(protocols) { + $scope.protocols = protocols; + }); + + // If we are editing an existing sharing profile, pull its data + if (identifier) { + + // Pull data from existing sharing profile + sharingProfileService.getSharingProfile($scope.selectedDataSource, identifier) + .success(function sharingProfileRetrieved(sharingProfile) { + $scope.sharingProfile = sharingProfile; + }); + + // Pull sharing profile parameters + sharingProfileService.getSharingProfileParameters($scope.selectedDataSource, identifier) + .success(function parametersReceived(parameters) { + $scope.parameters = parameters; + }); + + } + + // If we are cloning an existing sharing profile, pull its data instead + else if (cloneSourceIdentifier) { + + // Pull data from cloned sharing profile + sharingProfileService.getSharingProfile($scope.selectedDataSource, cloneSourceIdentifier) + .success(function sharingProfileRetrieved(sharingProfile) { + + // Store data of sharing profile being cloned + $scope.sharingProfile = sharingProfile; + + // Clear the identifier field because this sharing profile is new + delete $scope.sharingProfile.identifier; + + }); + + // Pull sharing profile parameters from cloned sharing profile + sharingProfileService.getSharingProfileParameters($scope.selectedDataSource, cloneSourceIdentifier) + .success(function parametersReceived(parameters) { + $scope.parameters = parameters; + }); + + } + + // If we are creating a new sharing profile, populate skeleton sharing + // profile data + else { + + $scope.sharingProfile = new SharingProfile({ + primaryConnectionIdentifier : $location.search().parent + }); + + $scope.parameters = {}; + + } + + // Populate primary connection once its identifier is known + $scope.$watch('sharingProfile.primaryConnectionIdentifier', + function retrievePrimaryConnection(identifier) { + + // Pull data from existing sharing profile + connectionService.getConnection($scope.selectedDataSource, identifier) + .success(function connectionRetrieved(connection) { + $scope.primaryConnection = connection; + }); + + }); + + /** + * Returns the translation string namespace for the protocol having the + * given name. The namespace will be of the form: + * + * PROTOCOL_NAME + * + * where NAME is the protocol name transformed via + * translationStringService.canonicalize(). + * + * @param {String} protocolName + * The name of the protocol. + * + * @returns {String} + * The translation namespace for the protocol specified, or null if no + * namespace could be generated. + */ + $scope.getNamespace = function getNamespace(protocolName) { + + // Do not generate a namespace if no protocol is selected + if (!protocolName) + return null; + + return 'PROTOCOL_' + translationStringService.canonicalize(protocolName); + + }; + + /** + * Cancels all pending edits, returning to the management page. + */ + $scope.cancel = function cancel() { + $location.url('/settings/' + encodeURIComponent($scope.selectedDataSource) + '/connections'); + }; + + /** + * Cancels all pending edits, opening an edit page for a new sharing profile + * which is prepopulated with the data from the sharing profile currently + * being edited. + */ + $scope.cloneSharingProfile = function cloneSharingProfile() { + $location.path('/manage/' + encodeURIComponent($scope.selectedDataSource) + '/sharingProfiles').search('clone', identifier); + }; + + /** + * Saves the sharing profile, creating a new sharing profile or updating + * the existing sharing profile. + */ + $scope.saveSharingProfile = function saveSharingProfile() { + + $scope.sharingProfile.parameters = $scope.parameters; + + // Save the sharing profile + sharingProfileService.saveSharingProfile($scope.selectedDataSource, $scope.sharingProfile) + .success(function savedSharingProfile() { + $location.url('/settings/' + encodeURIComponent($scope.selectedDataSource) + '/connections'); + }) + + // Notify of any errors + .error(function sharingProfileSaveFailed(error) { + guacNotification.showStatus({ + 'className' : 'error', + 'title' : 'MANAGE_SHARING_PROFILE.DIALOG_HEADER_ERROR', + 'text' : error.message, + 'actions' : [ ACKNOWLEDGE_ACTION ] + }); + }); + + }; + + /** + * An action to be provided along with the object sent to showStatus which + * immediately deletes the current sharing profile. + */ + var DELETE_ACTION = { + name : "MANAGE_SHARING_PROFILE.ACTION_DELETE", + className : "danger", + // Handle action + callback : function deleteCallback() { + deleteSharingProfileImmediately(); + guacNotification.showStatus(false); + } + }; + + /** + * Immediately deletes the current sharing profile, without prompting the + * user for confirmation. + */ + var deleteSharingProfileImmediately = function deleteSharingProfileImmediately() { + + // Delete the sharing profile + sharingProfileService.deleteSharingProfile($scope.selectedDataSource, $scope.sharingProfile) + .success(function deletedSharingProfile() { + $location.path('/settings/' + encodeURIComponent($scope.selectedDataSource) + '/connections'); + }) + + // Notify of any errors + .error(function sharingProfileDeletionFailed(error) { + guacNotification.showStatus({ + 'className' : 'error', + 'title' : 'MANAGE_SHARING_PROFILE.DIALOG_HEADER_ERROR', + 'text' : error.message, + 'actions' : [ ACKNOWLEDGE_ACTION ] + }); + }); + + }; + + /** + * Deletes the sharing profile, prompting the user first to confirm that + * deletion is desired. + */ + $scope.deleteSharingProfile = function deleteSharingProfile() { + + // Confirm deletion request + guacNotification.showStatus({ + 'title' : 'MANAGE_SHARING_PROFILE.DIALOG_HEADER_CONFIRM_DELETE', + 'text' : 'MANAGE_SHARING_PROFILE.TEXT_CONFIRM_DELETE', + 'actions' : [ DELETE_ACTION, CANCEL_ACTION] + }); + + }; + +}]); diff --git a/guacamole/src/main/webapp/app/manage/templates/manageSharingProfile.html b/guacamole/src/main/webapp/app/manage/templates/manageSharingProfile.html new file mode 100644 index 000000000..121e4fe49 --- /dev/null +++ b/guacamole/src/main/webapp/app/manage/templates/manageSharingProfile.html @@ -0,0 +1,44 @@ +
+ + +
+

{{'MANAGE_SHARING_PROFILE.SECTION_HEADER_EDIT_SHARING_PROFILE' | translate}}

+ +
+
+ + + + + + + + + +
{{'MANAGE_SHARING_PROFILE.FIELD_HEADER_NAME' | translate}}
{{'MANAGE_SHARING_PROFILE.FIELD_HEADER_PRIMARY_CONNECTION' | translate}}{{primaryConnection.name}}
+
+ + +
+ +
+ + +

{{'MANAGE_SHARING_PROFILE.SECTION_HEADER_PARAMETERS' | translate}}

+
+ +
+ + +
+ + + + +
+ +
diff --git a/guacamole/src/main/webapp/translations/de.json b/guacamole/src/main/webapp/translations/de.json index 67c8a1519..9bdf78730 100644 --- a/guacamole/src/main/webapp/translations/de.json +++ b/guacamole/src/main/webapp/translations/de.json @@ -231,6 +231,22 @@ }, + "MANAGE_SHARING_PROFILE" : { + + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", + "ACTION_CLONE" : "@:APP.ACTION_CLONE", + "ACTION_DELETE" : "@:APP.ACTION_DELETE", + "ACTION_SAVE" : "@:APP.ACTION_SAVE", + + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + + "FIELD_HEADER_NAME" : "Name:", + + "SECTION_HEADER_PARAMETERS" : "Parameter" + + }, + "MANAGE_USER" : { "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", diff --git a/guacamole/src/main/webapp/translations/en.json b/guacamole/src/main/webapp/translations/en.json index f934a22de..af3d8c576 100644 --- a/guacamole/src/main/webapp/translations/en.json +++ b/guacamole/src/main/webapp/translations/en.json @@ -238,6 +238,27 @@ }, + "MANAGE_SHARING_PROFILE" : { + + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", + "ACTION_CLONE" : "@:APP.ACTION_CLONE", + "ACTION_DELETE" : "@:APP.ACTION_DELETE", + "ACTION_SAVE" : "@:APP.ACTION_SAVE", + + "DIALOG_HEADER_CONFIRM_DELETE" : "Delete Sharing Profile", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + + "FIELD_HEADER_NAME" : "Name:", + "FIELD_HEADER_PRIMARY_CONNECTION" : "Primary Connection:", + + "SECTION_HEADER_EDIT_SHARING_PROFILE" : "Edit Sharing Profile", + "SECTION_HEADER_PARAMETERS" : "Parameters", + + "TEXT_CONFIRM_DELETE" : "Sharing profiles cannot be restored after they have been deleted. Are you sure you want to delete this sharing profile?" + + }, + "MANAGE_USER" : { "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", diff --git a/guacamole/src/main/webapp/translations/fr.json b/guacamole/src/main/webapp/translations/fr.json index b2199979e..ca4a5bdc5 100644 --- a/guacamole/src/main/webapp/translations/fr.json +++ b/guacamole/src/main/webapp/translations/fr.json @@ -231,6 +231,22 @@ }, + "MANAGE_SHARING_PROFILE" : { + + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", + "ACTION_CLONE" : "@:APP.ACTION_CLONE", + "ACTION_DELETE" : "@:APP.ACTION_DELETE", + "ACTION_SAVE" : "@:APP.ACTION_SAVE", + + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + + "FIELD_HEADER_NAME" : "Nom:", + + "SECTION_HEADER_PARAMETERS" : "Paramètres" + + }, + "MANAGE_USER" : { "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", diff --git a/guacamole/src/main/webapp/translations/it.json b/guacamole/src/main/webapp/translations/it.json index 45e800909..e84e67f48 100644 --- a/guacamole/src/main/webapp/translations/it.json +++ b/guacamole/src/main/webapp/translations/it.json @@ -219,6 +219,22 @@ }, + "MANAGE_SHARING_PROFILE" : { + + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", + "ACTION_CLONE" : "@:APP.ACTION_CLONE", + "ACTION_DELETE" : "@:APP.ACTION_DELETE", + "ACTION_SAVE" : "@:APP.ACTION_SAVE", + + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + + "FIELD_HEADER_NAME" : "Name:", + + "SECTION_HEADER_PARAMETERS" : "Parametri" + + }, + "MANAGE_USER" : { "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", diff --git a/guacamole/src/main/webapp/translations/nl.json b/guacamole/src/main/webapp/translations/nl.json index 413ebbd18..da1df107a 100644 --- a/guacamole/src/main/webapp/translations/nl.json +++ b/guacamole/src/main/webapp/translations/nl.json @@ -231,6 +231,22 @@ }, + "MANAGE_SHARING_PROFILE" : { + + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", + "ACTION_CLONE" : "@:APP.ACTION_CLONE", + "ACTION_DELETE" : "@:APP.ACTION_DELETE", + "ACTION_SAVE" : "@:APP.ACTION_SAVE", + + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + + "FIELD_HEADER_NAME" : "Naam:", + + "SECTION_HEADER_PARAMETERS" : "Parameters" + + }, + "MANAGE_USER" : { "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", diff --git a/guacamole/src/main/webapp/translations/ru.json b/guacamole/src/main/webapp/translations/ru.json index 8f2ef69b0..5e305a580 100644 --- a/guacamole/src/main/webapp/translations/ru.json +++ b/guacamole/src/main/webapp/translations/ru.json @@ -219,6 +219,22 @@ }, + "MANAGE_SHARING_PROFILE" : { + + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", + "ACTION_CLONE" : "@:APP.ACTION_CLONE", + "ACTION_DELETE" : "@:APP.ACTION_DELETE", + "ACTION_SAVE" : "@:APP.ACTION_SAVE", + + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + + "FIELD_HEADER_NAME" : "Название:", + + "SECTION_HEADER_PARAMETERS" : "Настройки" + + }, + "MANAGE_USER" : { "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE",