GUACAMOLE-926: Migrate import code to a dedicated module.

This commit is contained in:
James Muehlner
2023-02-01 23:25:12 +00:00
parent ec33c81f1a
commit fac76ef0cb
10 changed files with 361 additions and 59 deletions

View File

@@ -20,11 +20,11 @@
/** /**
* The controller for the connection import page. * The controller for the connection import page.
*/ */
angular.module('settings').controller('importConnectionsController', ['$scope', '$injector', angular.module('import').controller('importConnectionsController', ['$scope', '$injector',
function importConnectionsController($scope, $injector) { function importConnectionsController($scope, $injector) {
// Required services // Required services
const connectionImportParseService = $injector.get('connectionImportParseService'); const connectionParseService = $injector.get('connectionParseService');
const connectionService = $injector.get('connectionService'); const connectionService = $injector.get('connectionService');
function processData(type, data) { function processData(type, data) {
@@ -36,18 +36,18 @@ angular.module('settings').controller('importConnectionsController', ['$scope',
case "application/json": case "application/json":
case "text/json": case "text/json":
requestBody = connectionImportParseService.parseJSON(data); requestBody = connectionParseService.parseJSON(data);
break; break;
case "text/csv": case "text/csv":
requestBody = connectionImportParseService.parseCSV(data); requestBody = connectionParseService.parseCSV(data);
break; break;
case "application/yaml": case "application/yaml":
case "application/x-yaml": case "application/x-yaml":
case "text/yaml": case "text/yaml":
case "text/x-yaml": case "text/x-yaml":
requestBody = connectionImportParseService.parseYAML(data); requestBody = connectionParseService.parseYAML(data);
break; break;
} }

View File

@@ -0,0 +1,24 @@
/*
* 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 module for code supporting importing user-supplied files. Currently, only
* connection import is supported.
*/
angular.module('import', ['rest']);

View File

@@ -0,0 +1,103 @@
/*
* 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.
*/
/* global _ */
/**
* A service for parsing user-provided CSV connection data for bulk import.
*/
angular.module('import').factory('connectionCSVService',
['$injector', function connectionCSVService($injector) {
// Required services
const $q = $injector.get('$q');
const $routeParams = $injector.get('$routeParams');
const schemaService = $injector.get('schemaService');
const service = {};
/**
* Returns a promise that resolves to an object detailing the connection
* attributes for the current data source, as well as the connection
* paremeters for every protocol, for the current data source.
*
* The object that the promise will contain an "attributes" key that maps to
* a set of attribute names, and a "protocolParameters" key that maps to an
* object mapping protocol names to sets of parameter names for that protocol.
*
* The intended use case for this object is to determine if there is a
* connection parameter or attribute with a given name, by e.g. checking the
* path `.protocolParameters[protocolName]` to see if a protocol exists,
* checking the path `.protocolParameters[protocolName][fieldName]` to see
* if a parameter exists for a given protocol, or checking the path
* `.attributes[fieldName]` to check if a connection attribute exists.
*
* @returns {Promise.<Object>}
*/
function getFieldLookups() {
// The current data source - the one that the connections will be
// imported into
const dataSource = $routeParams.dataSource;
// Fetch connection attributes and protocols for the current data source
return $q.all({
attributes : schemaService.getConnectionAttributes(dataSource),
protocols : schemaService.getProtocols(dataSource)
})
.then(function connectionStructureRetrieved({attributes, protocols}) {
return {
// Translate the forms and fields into a flat map of attribute
// name to `true` boolean value
attributes: attributes.reduce(
(attributeMap, form) => {
form.fields.forEach(
field => attributeMap[field.name] = true);
return attributeMap
}, {}),
// Translate the protocol definitions into a map of protocol
// name to map of field name to `true` boolean value
protocolParameters: _.mapValues(
protocols, protocol => protocol.connectionForms.reduce(
(protocolFieldMap, form) => {
form.fields.forEach(
field => protocolFieldMap[field.name] = true);
return protocolFieldMap;
}, {}))
};
});
}
/**
*
*
* @returns {Promise.<Function.<String[], Object>>}
* A promise that will resolve to a function that translates a CSV data
* row (array of strings) to a connection object.
*/
service.getCSVTransformer = function getCSVTransformer(headerRow) {
};
return service;
}]);

View File

@@ -26,8 +26,8 @@ import { parse as parseYAMLData } from 'yaml'
* A service for parsing user-provided JSON, YAML, or JSON connection data into * A service for parsing user-provided JSON, YAML, or JSON connection data into
* an appropriate format for bulk uploading using the PATCH REST endpoint. * an appropriate format for bulk uploading using the PATCH REST endpoint.
*/ */
angular.module('settings').factory('connectionImportParseService', angular.module('import').factory('connectionParseService',
['$injector', function connectionImportParseService($injector) { ['$injector', function connectionParseService($injector) {
// Required types // Required types
const Connection = $injector.get('Connection'); const Connection = $injector.get('Connection');
@@ -71,35 +71,6 @@ angular.module('settings').factory('connectionImportParseService',
}) })
}); });
} }
/**
* Convert a provided YAML representation of a connection list into a JSON
* string to be submitted to the PATCH REST endpoint. The returned JSON
* string will contain a PATCH operation to create each connection in the
* provided list.
*
* @param {String} yamlData
* The YAML-encoded connection list to convert to a PATCH request body.
*
* @return {Promise.<Connection[]>}
* A promise resolving to an array of Connection objects, one for each
* connection in the provided CSV.
*/
service.parseYAML = function parseYAML(yamlData) {
// Parse from YAML into a javascript array
const parsedData = parseYAMLData(yamlData);
// Check that the data is the correct format, and not empty
performBasicChecks(parsedData);
// Convert to an array of Connection objects and return
const deferredConnections = $q.defer();
deferredConnections.resolve(
parsedData.map(connection => new Connection(connection)));
return deferredConnections.promise;
};
/** /**
* Returns a promise that resolves to an object detailing the connection * Returns a promise that resolves to an object detailing the connection
@@ -321,6 +292,35 @@ angular.module('settings').factory('connectionImportParseService',
}; };
/**
* Convert a provided YAML representation of a connection list into a JSON
* string to be submitted to the PATCH REST endpoint. The returned JSON
* string will contain a PATCH operation to create each connection in the
* provided list.
*
* @param {String} yamlData
* The YAML-encoded connection list to convert to a PATCH request body.
*
* @return {Promise.<Connection[]>}
* A promise resolving to an array of Connection objects, one for each
* connection in the provided CSV.
*/
service.parseYAML = function parseYAML(yamlData) {
// Parse from YAML into a javascript array
const parsedData = parseYAMLData(yamlData);
// Check that the data is the correct format, and not empty
performBasicChecks(parsedData);
// Convert to an array of Connection objects and return
const deferredConnections = $q.defer();
deferredConnections.resolve(
parsedData.map(connection => new Connection(connection)));
return deferredConnections.promise;
};
/** /**
* Convert a provided JSON representation of a connection list into a JSON * Convert a provided JSON representation of a connection list into a JSON
* string to be submitted to the PATCH REST endpoint. The returned JSON * string to be submitted to the PATCH REST endpoint. The returned JSON

View File

@@ -20,7 +20,7 @@
/** /**
* Service which defines the ParseError class. * Service which defines the ParseError class.
*/ */
angular.module('settings').factory('ParseError', [function defineParseError() { angular.module('import').factory('ParseError', [function defineParseError() {
/** /**
* An error representing a parsing failure when attempting to convert * An error representing a parsing failure when attempting to convert

View File

@@ -127,10 +127,10 @@ angular.module('index').config(['$routeProvider', '$locationProvider',
}) })
// Connection import page // Connection import page
.when('/settings/:dataSource/import', { .when('/import/:dataSource/connection', {
title : 'APP.NAME', title : 'APP.NAME',
bodyClassName : 'settings', bodyClassName : 'settings',
templateUrl : 'app/settings/templates/settingsImport.html', templateUrl : 'app/import/templates/connectionImport.html',
controller : 'importConnectionsController', controller : 'importConnectionsController',
resolve : { updateCurrentToken: updateCurrentToken } resolve : { updateCurrentToken: updateCurrentToken }
}) })

View File

@@ -156,41 +156,38 @@ angular.module('rest').factory('connectionService', ['$injector',
}; };
/** /**
* Makes a request to the REST API to create multiple connections, returning a * Makes a request to the REST API to apply a supplied list of connection
* a promise that can be used for processing the results of the call. This * patches, returning a promise that can be used for processing the results
* operation is atomic - if any errors are encountered during the connection * of the call.
* creation process, the entire request will fail, and no connections will be *
* created. * This operation is atomic - if any errors are encountered during the
* connection patching process, the entire request will fail, and no
* changes will be persisted.
* *
* @param {Connection[]} connections The connections to create. * @param {DirectoryPatch.<Connection>[]} patches
* An array of patches to apply.
* *
* @returns {Promise} * @returns {Promise}
* A promise for the HTTP call which will succeed if and only if the * A promise for the HTTP call which will succeed if and only if the
* create operation is successful. * patch operation is successful.
*/ */
service.createConnections = function createConnections(dataSource, connections) { service.patchConnections = function patchConnections(dataSource, patches) {
// An object containing a PATCH operation to create each connection // Make the PATCH request
const patchBody = connections.map(connection => ({
op: "add",
path: "/",
value: connection
}));
// Make a PATCH request to create the connections
return authenticationService.request({ return authenticationService.request({
method : 'PATCH', method : 'PATCH',
url : 'api/session/data/' + encodeURIComponent(dataSource) + '/connections', url : 'api/session/data/' + encodeURIComponent(dataSource) + '/connections',
data : patchBody data : patches
}) })
// Clear the cache // Clear the cache
.then(function connectionUpdated(){ .then(function connectionsPatched(){
cacheService.connections.removeAll(); cacheService.connections.removeAll();
// Clear users cache to force reload of permissions for this // Clear users cache to force reload of permissions for any
// newly updated connection // newly created or replaced connections
cacheService.users.removeAll(); cacheService.users.removeAll();
}); });
} }

View File

@@ -0,0 +1,95 @@
/*
* 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.
*/
/**
* Service which defines the DirectoryPatch class.
*/
angular.module('rest').factory('DirectoryPatch', [function defineDirectoryPatch() {
/**
* The object consumed by REST API calls when representing changes to an
* arbitrary set of directory-based objects.
* @constructor
*
* @template DirectoryObject
* The directory-based object type that this DirectoryPatch will
* operate on.
*
* @param {DirectoryObject|Object} [template={}]
* The object whose properties should be copied within the new
* DirectoryPatch.
*/
var DirectoryPatch = function DirectoryPatch(template) {
// Use empty object by default
template = template || {};
/**
* The operation to apply to the objects indicated by the path. Valid
* operation values are defined within DirectoryPatch.Operation.
*
* @type {String}
*/
this.op = template.op;
/**
* The path of the objects to modify. For creation of new objects, this
* should be "/". Otherwise, it should be "/{identifier}", specifying
* the identifier of the existing object being modified.
*
* @type {String}
* @default '/'
*/
this.path = template.path || '/';
/**
* The object being added or replaced, or the identifier of the object
* being removed.
*
* @type {DirectoryObject|String}
*/
this.value = template.value;
};
/**
* All valid patch operations for directory-based objects.
*/
DirectoryPatch.Operation = {
/**
* Adds the specified object to the relation.
*/
ADD : "add",
/**
* Removes the specified object from the relation.
*/
REPLACE : "replace",
/**
* Removes the specified object from the relation.
*/
REMOVE : "remove"
};
return DirectoryPatch;
}]);

View File

@@ -0,0 +1,83 @@
/*
* 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.
*/
/**
* Service which defines the DirectoryPatchOutcome class.
*/
angular.module('rest').factory('DirectoryPatchOutcome', [
function defineDirectoryPatchOutcome() {
/**
* An object returned by a PATCH request to a directory REST API,
* representing the outcome associated with a particular patch in the
* request. This object can indicate either a successful or unsuccessful
* response. The error field is only meaningful for unsuccessful patches.
* @constructor
*
* @template DirectoryObject
* The directory-based object type that this DirectoryPatchOutcome
* represents a patch outcome for.
*
* @param {DirectoryObject|Object} [template={}]
* The object whose properties should be copied within the new
* DirectoryPatchOutcome.
*/
var DirectoryPatchOutcome = function DirectoryPatchOutcome(template) {
// Use empty object by default
template = template || {};
/**
* The operation to apply to the objects indicated by the path. Valid
* operation values are defined within DirectoryPatch.Operation.
*
* @type {String}
*/
this.op = template.op;
/**
* The path of the object operated on by the corresponding patch in the
* request.
*
* @type {String}
*/
this.path = template.path;
/**
* The identifier of the object operated on by the corresponding patch
* in the request. If the object was newly created and the PATCH request
* did not fail, this will be the identifier of the newly created object.
*
* @type {String}
*/
this.identifier = template.identifier;
/**
* The error message associated with the failure, if the patch failed to
* apply.
*
* @type {String}
*/
this.error = template.error;
};
return DirectoryPatch;
}]);