GUACAMOLE-630: Merge editing of "argv" based parameters.

This commit is contained in:
James Muehlner
2019-08-08 20:33:29 -07:00
committed by GitHub
4 changed files with 308 additions and 8 deletions

View File

@@ -27,6 +27,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
var ManagedClient = $injector.get('ManagedClient'); var ManagedClient = $injector.get('ManagedClient');
var ManagedClientState = $injector.get('ManagedClientState'); var ManagedClientState = $injector.get('ManagedClientState');
var ManagedFilesystem = $injector.get('ManagedFilesystem'); var ManagedFilesystem = $injector.get('ManagedFilesystem');
var Protocol = $injector.get('Protocol');
var ScrollState = $injector.get('ScrollState'); var ScrollState = $injector.get('ScrollState');
// Required services // Required services
@@ -248,7 +249,15 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
* *
* @type ScrollState * @type ScrollState
*/ */
scrollState : new ScrollState() scrollState : new ScrollState(),
/**
* The current desired values of all editable connection parameters as
* a set of name/value pairs, including any changes made by the user.
*
* @type {Object.<String, String>}
*/
connectionParameters : {}
}; };
@@ -257,6 +266,16 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
$scope.menu.shown = false; $scope.menu.shown = false;
}; };
/**
* Applies any changes to connection parameters made by the user within the
* Guacamole menu.
*/
$scope.applyParameterChanges = function applyParameterChanges() {
angular.forEach($scope.menu.connectionParameters, function sendArgv(value, name) {
ManagedClient.setArgument($scope.client, name, value);
});
};
/** /**
* The client which should be attached to the client UI. * The client which should be attached to the client UI.
* *
@@ -429,11 +448,19 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
}); });
// Update client state/behavior as visibility of the Guacamole menu changes
$scope.$watch('menu.shown', function menuVisibilityChanged(menuShown, menuShownPreviousState) { $scope.$watch('menu.shown', function menuVisibilityChanged(menuShown, menuShownPreviousState) {
// Send clipboard data if menu is hidden // Send clipboard and argument value data once menu is hidden
if (!menuShown && menuShownPreviousState) if (!menuShown && menuShownPreviousState) {
$scope.$broadcast('guacClipboard', $scope.client.clipboardData); $scope.$broadcast('guacClipboard', $scope.client.clipboardData);
$scope.applyParameterChanges();
}
// Obtain snapshot of current editable connection parameters when menu
// is opened
else if (menuShown)
$scope.menu.connectionParameters = ManagedClient.getArgumentModel($scope.client);
// Disable client keyboard if the menu is shown // Disable client keyboard if the menu is shown
$scope.client.clientProperties.keyboardEnabled = !menuShown; $scope.client.clientProperties.keyboardEnabled = !menuShown;
@@ -805,6 +832,11 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
// Set client-specific menu actions // Set client-specific menu actions
$scope.clientMenuActions = [ DISCONNECT_MENU_ACTION ]; $scope.clientMenuActions = [ DISCONNECT_MENU_ACTION ];
/**
* @borrows Protocol.getNamespace
*/
$scope.getProtocolNamespace = Protocol.getNamespace;
/** /**
* The currently-visible filesystem within the filesystem menu, if the * The currently-visible filesystem within the filesystem menu, if the
* filesystem menu is open. If no filesystem is currently visible, this * filesystem menu is open. If no filesystem is currently visible, this

View File

@@ -96,6 +96,14 @@
</div> </div>
</div> </div>
<!-- Connection parameters which may be modified while the connection is open -->
<div class="menu-section connection-parameters" id="connection-settings" ng-show="client.protocol">
<guac-form namespace="getProtocolNamespace(client.protocol)"
content="client.forms"
model="menu.connectionParameters"
model-only="true"></guac-form>
</div>
<!-- Input method --> <!-- Input method -->
<div class="menu-section" id="keyboard-settings"> <div class="menu-section" id="keyboard-settings">
<h3>{{'CLIENT.SECTION_HEADER_INPUT_METHOD' | translate}}</h3> <h3>{{'CLIENT.SECTION_HEADER_INPUT_METHOD' | translate}}</h3>

View File

@@ -0,0 +1,152 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* Provides the ManagedArgument class used by ManagedClient.
*/
angular.module('client').factory('ManagedArgument', ['$q', function defineManagedArgument($q) {
/**
* Object which represents an argument (connection parameter) which may be
* changed by the user while the connection is open.
*
* @constructor
* @param {ManagedArgument|Object} [template={}]
* The object whose properties should be copied within the new
* ManagedArgument.
*/
var ManagedArgument = function ManagedArgument(template) {
// Use empty object by default
template = template || {};
/**
* The name of the connection parameter.
*
* @type {String}
*/
this.name = template.name;
/**
* The current value of the connection parameter.
*
* @type {String}
*/
this.value = template.value;
/**
* A valid, open output stream which may be used to apply a new value
* to the connection parameter.
*
* @type {Guacamole.OutputStream}
*/
this.stream = template.stream;
};
/**
* Requests editable access to a given connection parameter, returning a
* promise which is resolved with a ManagedArgument instance that provides
* such access if the parameter is indeed editable.
*
* @param {ManagedClient} managedClient
* The ManagedClient instance associated with the connection for which
* an editable version of the connection parameter is being retrieved.
*
* @param {String} name
* The name of the connection parameter.
*
* @param {String} value
* The current value of the connection parameter, as received from a
* prior, inbound "argv" stream.
*
* @returns {Promise.<ManagedArgument>}
* A promise which is resolved with the new ManagedArgument instance
* once the requested parameter has been verified as editable.
*/
ManagedArgument.getInstance = function getInstance(managedClient, name, value) {
var deferred = $q.defer();
// Create internal, fully-populated instance of ManagedArgument, to be
// returned only once mutability of the associated connection parameter
// has been verified
var managedArgument = new ManagedArgument({
name : name,
value : value,
stream : managedClient.client.createArgumentValueStream('text/plain', name)
});
// The connection parameter is editable only if a successful "ack" is
// received
managedArgument.stream.onack = function ackReceived(status) {
if (status.isError())
deferred.reject(status);
else
deferred.resolve(managedArgument);
};
return deferred.promise;
};
/**
* Sets the given editable argument (connection parameter) to the given
* value, updating the behavior of the associated connection in real-time.
* If successful, the ManagedArgument provided cannot be used for future
* calls to setValue() and must be replaced with a new instance. This
* function only has an effect if the new parameter value is different from
* the current value.
*
* @param {ManagedArgument} managedArgument
* The ManagedArgument instance associated with the connection
* parameter being modified.
*
* @param {String} value
* The new value to assign to the connection parameter.
*
* @returns {Boolean}
* true if the connection parameter was sent and the provided
* ManagedArgument instance may no longer be used for future setValue()
* calls, false if the connection parameter was NOT sent as it has not
* changed.
*/
ManagedArgument.setValue = function setValue(managedArgument, value) {
// Stream new value only if value has changed
if (value !== managedArgument.value) {
var writer = new Guacamole.StringWriter(managedArgument.stream);
writer.sendText(value);
writer.sendEnd();
// ManagedArgument instance is no longer usable
return true;
}
// No parameter value change was attempted and the ManagedArgument
// instance may be reused
return false;
};
return ManagedArgument;
}]);

View File

@@ -27,6 +27,7 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
var ClientProperties = $injector.get('ClientProperties'); var ClientProperties = $injector.get('ClientProperties');
var ClientIdentifier = $injector.get('ClientIdentifier'); var ClientIdentifier = $injector.get('ClientIdentifier');
var ClipboardData = $injector.get('ClipboardData'); var ClipboardData = $injector.get('ClipboardData');
var ManagedArgument = $injector.get('ManagedArgument');
var ManagedClientState = $injector.get('ManagedClientState'); var ManagedClientState = $injector.get('ManagedClientState');
var ManagedClientThumbnail = $injector.get('ManagedClientThumbnail'); var ManagedClientThumbnail = $injector.get('ManagedClientThumbnail');
var ManagedDisplay = $injector.get('ManagedDisplay'); var ManagedDisplay = $injector.get('ManagedDisplay');
@@ -44,6 +45,7 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
var connectionService = $injector.get('connectionService'); var connectionService = $injector.get('connectionService');
var preferenceService = $injector.get('preferenceService'); var preferenceService = $injector.get('preferenceService');
var requestService = $injector.get('requestService'); var requestService = $injector.get('requestService');
var schemaService = $injector.get('schemaService');
var tunnelService = $injector.get('tunnelService'); var tunnelService = $injector.get('tunnelService');
var guacAudio = $injector.get('guacAudio'); var guacAudio = $injector.get('guacAudio');
var guacHistory = $injector.get('guacHistory'); var guacHistory = $injector.get('guacHistory');
@@ -117,6 +119,23 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
*/ */
this.title = template.title; this.title = template.title;
/**
* The name which uniquely identifies the protocol of the connection in
* use. If the protocol cannot be determined, such as when a connection
* group is in use, this will be null.
*
* @type {String}
*/
this.protocol = template.protocol || null;
/**
* An array of forms describing all known parameters for the connection
* in use, including those which may not be editable.
*
* @type {Form[]}
*/
this.forms = template.forms || [];
/** /**
* The most recently-generated thumbnail for this connection, as * The most recently-generated thumbnail for this connection, as
* stored within the local connection history. If no thumbnail is * stored within the local connection history. If no thumbnail is
@@ -179,6 +198,17 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
*/ */
this.clientProperties = template.clientProperties || new ClientProperties(); this.clientProperties = template.clientProperties || new ClientProperties();
/**
* All editable arguments (connection parameters), stored by their
* names. Arguments will only be present within this set if their
* current values have been exposed by the server via an inbound "argv"
* stream and the server has confirmed that the value may be changed
* through a successful "ack" to an outbound "argv" stream.
*
* @type {Object.<String, ManagedArgument>}
*/
this.arguments = template.arguments || {};
}; };
/** /**
@@ -448,6 +478,33 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
}; };
// Test for argument mutability whenever an argument value is
// received
client.onargv = function clientArgumentValueReceived(stream, mimetype, name) {
// Ignore arguments which do not use a mimetype currently supported
// by the web application
if (mimetype !== 'text/plain')
return;
var reader = new Guacamole.StringReader(stream);
// Assemble received data into a single string
var value = '';
reader.ontext = function textReceived(text) {
value += text;
};
// Test mutability once stream is finished, storing the current
// value for the argument only if it is mutable
reader.onend = function textComplete() {
ManagedArgument.getInstance(managedClient, name, value).then(function argumentIsMutable(argument) {
managedClient.arguments[name] = argument;
}, function ignoreImmutableArguments() {});
};
};
// Handle any received clipboard data // Handle any received clipboard data
client.onclipboard = function clientClipboardReceived(stream, mimetype) { client.onclipboard = function clientClipboardReceived(stream, mimetype) {
@@ -522,11 +579,16 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
client.connect(connectString); client.connect(connectString);
}); });
// If using a connection, pull connection name // If using a connection, pull connection name and protocol information
if (clientIdentifier.type === ClientIdentifier.Types.CONNECTION) { if (clientIdentifier.type === ClientIdentifier.Types.CONNECTION) {
connectionService.getConnection(clientIdentifier.dataSource, clientIdentifier.id) $q.all({
.then(function connectionRetrieved(connection) { connection : connectionService.getConnection(clientIdentifier.dataSource, clientIdentifier.id),
managedClient.name = managedClient.title = connection.name; protocols : schemaService.getProtocols(clientIdentifier.dataSource)
})
.then(function dataRetrieved(values) {
managedClient.name = managedClient.title = values.connection.name;
managedClient.protocol = values.connection.protocol;
managedClient.forms = values.protocols[values.connection.protocol].connectionForms;
}, requestService.WARN); }, requestService.WARN);
} }
@@ -619,6 +681,52 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
}; };
/**
* Assigns the given value to the connection parameter having the given
* name, updating the behavior of the connection in real-time. If the
* connection parameter is not editable, this function has no effect.
*
* @param {ManagedClient} managedClient
* The ManagedClient instance associated with the active connection
* being modified.
*
* @param {String} name
* The name of the connection parameter to modify.
*
* @param {String} value
* The value to attempt to assign to the given connection parameter.
*/
ManagedClient.setArgument = function setArgument(managedClient, name, value) {
var managedArgument = managedClient.arguments[name];
if (managedArgument && ManagedArgument.setValue(managedArgument, value))
delete managedClient.arguments[name];
};
/**
* Retrieves the current values of all editable connection parameters as a
* set of name/value pairs suitable for use as the model of a form which
* edits those parameters.
*
* @param {ManagedClient} client
* The ManagedClient instance associated with the active connection
* whose parameter values are being retrieved.
*
* @returns {Object.<String, String>}
* A new set of name/value pairs containing the current values of all
* editable parameters.
*/
ManagedClient.getArgumentModel = function getArgumentModel(client) {
var model = {};
angular.forEach(client.arguments, function addModelEntry(managedArgument) {
model[managedArgument.name] = managedArgument.value;
});
return model;
};
/** /**
* Produces a sharing link for the given ManagedClient using the given * Produces a sharing link for the given ManagedClient using the given
* sharing profile. The resulting sharing link, and any required login * sharing profile. The resulting sharing link, and any required login