From 322adbc294c2189019e84a278017fdf85d309658 Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Thu, 23 Feb 2023 19:44:46 +0000 Subject: [PATCH] GUACAMOLE-926: Display connection-specific errors to the user. --- .../importConnectionsController.js | 57 ++-- .../directives/connectionImportErrors.js | 248 ++++++++++++++++++ .../directives/connectionImportFileUpload.js | 4 +- .../frontend/src/app/import/importModule.js | 2 +- .../import/services/connectionCSVService.js | 12 +- .../import/services/connectionParseService.js | 8 +- .../frontend/src/app/import/styles/import.css | 17 +- .../import/templates/connectionErrors.html | 45 ++++ .../import/templates/connectionImport.html | 9 +- .../templates/connectionImportFileHelp.html | 24 +- .../templates/connectionImportFileUpload.html | 8 +- .../src/app/import/types/DisplayErrorList.js | 83 ++++++ .../src/app/import/types/ImportConnection.js | 2 +- .../app/import/types/ImportConnectionError.js | 108 ++++++++ .../app/rest/services/connectionService.js | 4 +- .../main/frontend/src/app/rest/types/Error.js | 9 + .../main/frontend/src/translations/en.json | 16 +- 17 files changed, 598 insertions(+), 58 deletions(-) create mode 100644 guacamole/src/main/frontend/src/app/import/directives/connectionImportErrors.js create mode 100644 guacamole/src/main/frontend/src/app/import/templates/connectionErrors.html create mode 100644 guacamole/src/main/frontend/src/app/import/types/DisplayErrorList.js create mode 100644 guacamole/src/main/frontend/src/app/import/types/ImportConnectionError.js diff --git a/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js b/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js index 73acb1cc3..ec1227e95 100644 --- a/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js +++ b/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js @@ -49,6 +49,21 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ */ $scope.error = null; + /** + * The result of parsing the current upload, if successful. + * + * @type {ParseResult} + */ + $scope.parseResult = null; + + /** + * The failure associated with the current attempt to create connections + * through the API, if any. + * + * @type {Error} + */ + $scope.patchFailure = null;; + /** * True if the file is fully uploaded and ready to be processed, or false * otherwise. @@ -100,6 +115,8 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ $scope.fileData = null; $scope.mimeType = null; $scope.fileReader = null; + $scope.parseResult = null; + $scope.patchFailure = null; // Broadcast an event to clear the file upload UI $scope.$broadcast('clearFile'); @@ -254,10 +271,7 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ function cleanUpConnections(creationResponse) { return connectionService.patchConnections( - $routeParams.dataSource, createDeletionPatches(creationResponse)) - - // TODO: Better error handling? Make additional cleanup requests? - .catch(handleError); + $routeParams.dataSource, createDeletionPatches(creationResponse)); } @@ -274,10 +288,7 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ function cleanUpUsers(creationResponse) { return userService.patchUsers( - $routeParams.dataSource, createDeletionPatches(creationResponse)) - - // TODO: Better error handling? Make additional cleanup requests? - .catch(handleError); + $routeParams.dataSource, createDeletionPatches(creationResponse)); } @@ -294,10 +305,7 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ function cleanUpUserGroups(creationResponse) { return userGroupService.patchUserGroups( - $routeParams.dataSource, createDeletionPatches(creationResponse)) - - // TODO: Better error handling? Make additional cleanup requests? - .catch(handleError); + $routeParams.dataSource, createDeletionPatches(creationResponse)); } @@ -352,6 +360,15 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ */ function handleParseSuccess(parseResult) { + $scope.processing = false; + $scope.parseResult = parseResult; + + // If errors were encounted during file parsing, abort further + // processing - the user will have a chance to fix the errors and try + // again + if (parseResult.hasErrors) + return; + const dataSource = $routeParams.dataSource; console.log("parseResult", parseResult); @@ -372,7 +389,12 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ .then(resetUploadState) )); - }); + }) + + // If an error occured when the call to create the connections was made, + // skip any further processing - the user will have a chance to fix the + // problems and try again + .catch(patchFailure => { $scope.patchFailure = patchFailure; }); } /** @@ -433,7 +455,7 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ else { handleError(new ParseError({ message: 'Invalid file type: ' + mimeType, - key: 'CONNECTION_IMPORT.INVALID_FILE_TYPE', + key: 'IMPORT.INVALID_FILE_TYPE', variables: { TYPE: mimeType } })); return; @@ -459,8 +481,8 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ /** * @return {Boolean} - * True if import should be disabled, or false if cancellation - * should be allowed. + * True if import should be disabled, or false if import should be + * allowed. */ $scope.importDisabled = () => @@ -471,7 +493,8 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ $scope.processing; /** - * Cancel any in-progress upload, or clear any uploaded-but + * Cancel any in-progress upload, or clear any uploaded-but-errored-out + * batch. */ $scope.cancel = function() { diff --git a/guacamole/src/main/frontend/src/app/import/directives/connectionImportErrors.js b/guacamole/src/main/frontend/src/app/import/directives/connectionImportErrors.js new file mode 100644 index 000000000..dcf42695a --- /dev/null +++ b/guacamole/src/main/frontend/src/app/import/directives/connectionImportErrors.js @@ -0,0 +1,248 @@ +/* + * 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 directive that displays errors that occured during parsing of a connection + * import file, or errors that were returned from the API during the connection + * batch creation attempt. + */ +angular.module('import').directive('connectionImportErrors', [ + function connectionImportErrors() { + + const directive = { + restrict: 'E', + replace: true, + templateUrl: 'app/import/templates/connectionErrors.html', + scope: { + + /** + * The result of parsing the import file. Any errors in this file + * will be displayed to the user. + * + * @type ParseResult + */ + parseResult : '=', + + /** + * The error associated with an attempt to batch create the + * connections represented by the ParseResult, if the ParseResult + * had no errors. If the provided ParseResult has errors, no request + * should have been made, and any provided patch error will be + * ignored. + * + * @type Error + */ + patchFailure : '=', + + } + }; + + directive.controller = ['$scope', '$injector', + function connectionImportErrorsController($scope, $injector) { + + // Required types + const DisplayErrorList = $injector.get('DisplayErrorList'); + const ImportConnectionError = $injector.get('ImportConnectionError'); + const ParseError = $injector.get('ParseError'); + const SortOrder = $injector.get('SortOrder'); + + // Required services + const $q = $injector.get('$q'); + const $translate = $injector.get('$translate'); + + // There are errors to display if the parse result generated errors, or + // if the patch request failed + $scope.hasErrors = () => + !!_.get($scope, 'parseResult.hasErrors') || !!$scope.patchFailure; + + /** + * All connections with their associated errors for display. These may + * be either parsing failures, or errors returned from the API. Both + * error types will be adapted to a common display format, though the + * error types will never be mixed, because no REST request should ever + * be made if there are client-side parse errors. + * + * @type {ImportConnectionError[]} + */ + $scope.connectionErrors = []; + + /** + * SortOrder instance which maintains the sort order of the visible + * connection errors. + * + * @type SortOrder + */ + $scope.errorOrder = new SortOrder([ + 'rowNumber', + 'name', + 'protocol', + 'errors', + ]); + + /** + * Array of all connection error properties that are filterable. + * + * @type String[] + */ + $scope.filteredErrorProperties = [ + 'rowNumber', + 'name', + 'protocol', + 'errors', + ]; + + /** + * Generate a ImportConnectionError representing any errors associated + * with the row at the given index within the given parse result. + * + * @param {ParseResult} parseResult + * The result of parsing the connection import file. + * + * @param {Integer} index + * The current row within the import file, 0-indexed. + * + * @returns {ImportConnectionError} + * The connection error object associated with the given row in the + * given parse result. + */ + const generateConnectionError = (parseResult, index) => { + + // Get the patch associated with the current row + const patch = parseResult.patches[index]; + + // The value of a patch is just the Connection object + const connection = patch.value; + + return new ImportConnectionError({ + + // Add 1 to the index to get the position in the file + rowNumber: index + 1, + + // Basic connection information - name and protocol. + name: connection.name, + protocol: connection.protocol, + + // The group and parent identifiers, if any are set. Include + // both since these could be a potential source of conflict. + // TODO: Should we _really_ have both of these here? + group: connection.group, + parentIdentifier: connection.parentIdentifier, + + // Get the list of user and group identifiers from the parse + // result. There should one entry in each of these lists for + // each patch. + users: parseResult.users[index], + groups: parseResult.groups[index], + + // The human-readable error messages + errors: new DisplayErrorList( + [ ...(parseResult.errors[index] || []) ]) + }); + }; + + // If a new connection patch failure is seen, update the display list + $scope.$watch('patchFailure', async function patchFailureChanged(patchFailure) { + + const { parseResult } = $scope; + + // Do not attempt to process anything before the data has loaded + if (!patchFailure || !parseResult) + return; + + // Set up the list of connection errors based on the existing parse + // result, with error messages fetched from the patch failure + $scope.connectionErrors = parseResult.patches.map( + (patch, index) => { + + // Generate a connection error for display + const connectionError = generateConnectionError(parseResult, index); + + // Set the error from the PATCH request, if there is one + // TODO: These generally aren't translated from the backend - + // should we even bother trying to translate them? + const error = _.get(patchFailure, ['patches', index, 'error']); + if (error) + connectionError.errors = new DisplayErrorList([error]); + + return connectionError; + }); + }); + + // If a new parse result with errors is seen, update the display list + $scope.$watch('parseResult', async function parseResultChanged(parseResult) { + + // Do not process if there are no errors in the provided result + if (!parseResult || !parseResult.hasErrors) + return; + + // All promises from all translation requests. The scope will not be + // updated until all translations are ready. + const translationPromises = []; + + // The parse result should only be updated on a fresh file import; + // therefore it should be safe to skip checking the patch errors + // entirely - if set, they will be from the previous file and no + // longer relevant. + + // Set up the list of connection errors based on the updated parse + // result + const connectionErrors = parseResult.patches.map( + (patch, index) => { + + // Generate a connection error for display + const connectionError = generateConnectionError(parseResult, index); + + // Go through the errors and check if any are translateable + connectionError.errors.getArray().forEach( + (error, errorIndex) => { + + // If this error is a ParseError, it can be translated. + // NOTE: Generally one would translate error messages in the + // template, but in this case, the connection errors need to + // be raw strings in order to enable sorting and filtering. + if (error instanceof ParseError) + + // Fetch the translation and update it when it's ready + translationPromises.push($translate( + error.key, error.variables) + .then(translatedError => { + connectionError.errors.getArray()[errorIndex] = translatedError; + })); + + }); + + return connectionError; + + }); + + // Once all the translations have been completed, update the + // connectionErrors all in one go, to ensure no excessive reloading + $q.all(translationPromises).then(() => { + $scope.connectionErrors = connectionErrors; + }); + + }); + + }]; + + return directive; + +}]); \ No newline at end of file diff --git a/guacamole/src/main/frontend/src/app/import/directives/connectionImportFileUpload.js b/guacamole/src/main/frontend/src/app/import/directives/connectionImportFileUpload.js index 4e2c76b24..196e8f48f 100644 --- a/guacamole/src/main/frontend/src/app/import/directives/connectionImportFileUpload.js +++ b/guacamole/src/main/frontend/src/app/import/directives/connectionImportFileUpload.js @@ -170,7 +170,7 @@ angular.module('import').directive('connectionImportFileUpload', [ // If the provided file is not one of the supported types, // display an error and abort processing - setError('CONNECTION_IMPORT.ERROR_INVALID_FILE_TYPE', + setError('IMPORT.ERROR_INVALID_FILE_TYPE', { TYPE: mimeType }); return; } @@ -204,7 +204,7 @@ angular.module('import').directive('connectionImportFileUpload', [ // If more than one file was provided, print an error explaining // that only a single file is allowed and abort processing - setError('CONNECTION_IMPORT.ERROR_FILE_SINGLE_ONLY'); + setError('IMPORT.ERROR_FILE_SINGLE_ONLY'); return; } diff --git a/guacamole/src/main/frontend/src/app/import/importModule.js b/guacamole/src/main/frontend/src/app/import/importModule.js index 6480d62fc..46e5fa157 100644 --- a/guacamole/src/main/frontend/src/app/import/importModule.js +++ b/guacamole/src/main/frontend/src/app/import/importModule.js @@ -21,4 +21,4 @@ * The module for code supporting importing user-supplied files. Currently, only * connection import is supported. */ -angular.module('import', ['rest']); +angular.module('import', ['rest', 'list']); diff --git a/guacamole/src/main/frontend/src/app/import/services/connectionCSVService.js b/guacamole/src/main/frontend/src/app/import/services/connectionCSVService.js index 492b9388b..389749555 100644 --- a/guacamole/src/main/frontend/src/app/import/services/connectionCSVService.js +++ b/guacamole/src/main/frontend/src/app/import/services/connectionCSVService.js @@ -238,7 +238,7 @@ angular.module('import').factory('connectionCSVService', deferred.reject(new ParseError({ message: 'Duplicate CSV Header: ' + header, translatableMessage: new TranslatableMessage({ - key: 'CONNECTION_IMPORT.ERROR_DUPLICATE_CSV_HEADER', + key: 'IMPORT.ERROR_DUPLICATE_CSV_HEADER', variables: { HEADER: header } }) })); @@ -342,7 +342,7 @@ angular.module('import').factory('connectionCSVService', if (isAttribute && isParameter) throw new ParseError({ message: 'Ambiguous CSV Header: ' + header, - key: 'CONNECTION_IMPORT.ERROR_AMBIGUOUS_CSV_HEADER', + key: 'IMPORT.ERROR_AMBIGUOUS_CSV_HEADER', variables: { HEADER: header } }); @@ -350,7 +350,7 @@ angular.module('import').factory('connectionCSVService', else if (!isAttribute && !isParameter) throw new ParseError({ message: 'Invalid CSV Header: ' + header, - key: 'CONNECTION_IMPORT.ERROR_INVALID_CSV_HEADER', + key: 'IMPORT.ERROR_INVALID_CSV_HEADER', variables: { HEADER: header } }); @@ -372,21 +372,21 @@ angular.module('import').factory('connectionCSVService', if (!nameGetter) return deferred.reject(new ParseError({ message: 'The connection name must be provided', - key: 'CONNECTION_IMPORT.ERROR_REQUIRED_NAME' + key: 'IMPORT.ERROR_REQUIRED_NAME' })); // Fail if the protocol wasn't provided if (!protocolGetter) return deferred.reject(new ParseError({ message: 'The connection protocol must be provided', - key: 'CONNECTION_IMPORT.ERROR_REQUIRED_PROTOCOL' + key: 'IMPORT.ERROR_REQUIRED_PROTOCOL' })); // If both are specified, the parent group is ambigious if (parentIdentifierGetter && groupGetter) throw new ParseError({ message: 'Only one of group or parentIdentifier can be set', - key: 'CONNECTION_IMPORT.ERROR_AMBIGUOUS_PARENT_GROUP' + key: 'IMPORT.ERROR_AMBIGUOUS_PARENT_GROUP' }); // The function to transform a CSV row into a connection object diff --git a/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js b/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js index cf15a374e..dd9f49593 100644 --- a/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js +++ b/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js @@ -60,7 +60,7 @@ angular.module('import').factory('connectionParseService', if (!(parsedData instanceof Array)) return new ParseError({ message: 'Import data must be a list of connections', - key: 'CONNECTION_IMPORT.ERROR_ARRAY_REQUIRED' + key: 'IMPORT.ERROR_ARRAY_REQUIRED' }); // Make sure that the connection list is not empty - contains at least @@ -68,7 +68,7 @@ angular.module('import').factory('connectionParseService', if (!parsedData.length) return new ParseError({ message: 'The provided file is empty', - key: 'CONNECTION_IMPORT.ERROR_EMPTY_FILE' + key: 'IMPORT.ERROR_EMPTY_FILE' }); } @@ -148,7 +148,7 @@ angular.module('import').factory('connectionParseService', if (connection.parentIdentifier) throw new ParseError({ message: 'Only one of group or parentIdentifier can be set', - key: 'CONNECTION_IMPORT.ERROR_AMBIGUOUS_PARENT_GROUP' + key: 'IMPORT.ERROR_AMBIGUOUS_PARENT_GROUP' }); // Look up the parent identifier for the specified group path @@ -158,7 +158,7 @@ angular.module('import').factory('connectionParseService', if (!identifier) throw new ParseError({ message: 'No group found named: ' + connection.group, - key: 'CONNECTION_IMPORT.ERROR_INVALID_GROUP', + key: 'IMPORT.ERROR_INVALID_GROUP', variables: { GROUP: connection.group } }); diff --git a/guacamole/src/main/frontend/src/app/import/styles/import.css b/guacamole/src/main/frontend/src/app/import/styles/import.css index 5addb1933..8f038e4d1 100644 --- a/guacamole/src/main/frontend/src/app/import/styles/import.css +++ b/guacamole/src/main/frontend/src/app/import/styles/import.css @@ -27,5 +27,18 @@ display: flex; gap: 10px; justify-content: center; - -} \ No newline at end of file + +} + + +.import .errors table { + width: 100%; +} + +.import .errors .error-message { + color: red; +} + +.import .errors .error-message ul { + margin: 0px; +} diff --git a/guacamole/src/main/frontend/src/app/import/templates/connectionErrors.html b/guacamole/src/main/frontend/src/app/import/templates/connectionErrors.html new file mode 100644 index 000000000..eee23c260 --- /dev/null +++ b/guacamole/src/main/frontend/src/app/import/templates/connectionErrors.html @@ -0,0 +1,45 @@ +
+ + + + + + + + + + + + + + + + + + + + + + +
+ {{'IMPORT.TABLE_HEADER_ROW_NUMBER' | translate}} + + {{'IMPORT.TABLE_HEADER_NAME' | translate}} + + {{'IMPORT.TABLE_HEADER_PROTOCOL' | translate}} + + {{'IMPORT.TABLE_HEADER_ERRORS' | translate}} +
{{error.rowNumber}}{{error.name}}{{error.protocol}} +
    +
  • + {{ message }} +
  • +
+
+ + + +
diff --git a/guacamole/src/main/frontend/src/app/import/templates/connectionImport.html b/guacamole/src/main/frontend/src/app/import/templates/connectionImport.html index 881cb2429..d0f9fd537 100644 --- a/guacamole/src/main/frontend/src/app/import/templates/connectionImport.html +++ b/guacamole/src/main/frontend/src/app/import/templates/connectionImport.html @@ -1,7 +1,7 @@
-

{{'CONNECTION_IMPORT.HEADER' | translate}}

+

{{'IMPORT.HEADER' | translate}}

@@ -10,11 +10,11 @@
@@ -29,5 +29,8 @@ {{error.message}}

+ + +
diff --git a/guacamole/src/main/frontend/src/app/import/templates/connectionImportFileHelp.html b/guacamole/src/main/frontend/src/app/import/templates/connectionImportFileHelp.html index e6063e89f..8e88d235a 100644 --- a/guacamole/src/main/frontend/src/app/import/templates/connectionImportFileHelp.html +++ b/guacamole/src/main/frontend/src/app/import/templates/connectionImportFileHelp.html @@ -1,25 +1,25 @@
-

{{'CONNECTION_IMPORT.HELP_HEADER' | translate}}

+

{{'IMPORT.HELP_HEADER' | translate}}

-

{{'CONNECTION_IMPORT.HELP_FILE_TYPE_HEADER' | translate}}

-

{{'CONNECTION_IMPORT.HELP_FILE_TYPE_DESCRIPTION' | translate}}

+

{{'IMPORT.HELP_FILE_TYPE_HEADER' | translate}}

+

{{'IMPORT.HELP_FILE_TYPE_DESCRIPTION' | translate}}

-

{{'CONNECTION_IMPORT.HELP_CSV_HEADER' | translate}}

-

{{'CONNECTION_IMPORT.HELP_CSV_DESCRIPTION' | translate}}

-

{{'CONNECTION_IMPORT.HELP_CSV_MORE_DETAILS' | translate}}

+

{{'IMPORT.HELP_CSV_HEADER' | translate}}

+

{{'IMPORT.HELP_CSV_DESCRIPTION' | translate}}

+

{{'IMPORT.HELP_CSV_MORE_DETAILS' | translate}}

name,protocol,hostname,group,users,groups,guacd-encryption (attribute)
 conn1,vnc,conn1.web.com,ROOT,guac user 1;guac user 2,Connection 1 Users,none
 conn2,rdp,conn2.web.com,ROOT/Parent Group,guac user 1,,ssl
 conn3,ssh,conn3.web.com,ROOT/Parent Group/Child Group,guac user 2;guac user 3,,
 conn4,kubernetes,,,,,
-

{{'CONNECTION_IMPORT.HELP_JSON_HEADER' | translate}}

-

{{'CONNECTION_IMPORT.HELP_JSON_DESCRIPTION' | translate}}

-

{{'CONNECTION_IMPORT.HELP_JSON_MORE_DETAILS' | translate}}

+

{{'IMPORT.HELP_JSON_HEADER' | translate}}

+

{{'IMPORT.HELP_JSON_DESCRIPTION' | translate}}

+

{{'IMPORT.HELP_JSON_MORE_DETAILS' | translate}}

[
   {
     "name": "conn1",
@@ -51,8 +51,8 @@ conn4,kubernetes,,,,,
} ] -

{{'CONNECTION_IMPORT.HELP_YAML_HEADER' | translate}}

-

{{'CONNECTION_IMPORT.HELP_YAML_DESCRIPTION' | translate}}

+

{{'IMPORT.HELP_YAML_HEADER' | translate}}

+

{{'IMPORT.HELP_YAML_DESCRIPTION' | translate}}

---
   - name: conn1
     protocol: vnc
@@ -87,7 +87,7 @@ conn4,kubernetes,,,,,
protocol: kubernetes
    -
  1. {{'CONNECTION_IMPORT.HELP_SEMICOLON_FOOTNOTE' | translate}}
  2. +
  3. {{'IMPORT.HELP_SEMICOLON_FOOTNOTE' | translate}}
diff --git a/guacamole/src/main/frontend/src/app/import/templates/connectionImportFileUpload.html b/guacamole/src/main/frontend/src/app/import/templates/connectionImportFileUpload.html index 08dbaf511..188567c00 100644 --- a/guacamole/src/main/frontend/src/app/import/templates/connectionImportFileUpload.html +++ b/guacamole/src/main/frontend/src/app/import/templates/connectionImportFileUpload.html @@ -1,20 +1,20 @@
- {{'CONNECTION_IMPORT.UPLOAD_FILE_TYPES' | translate}} + {{'IMPORT.UPLOAD_FILE_TYPES' | translate}} {{'CONNECTION_IMPORT.UPLOAD_HELP_LINK' | translate}} + class="file-help-link">{{'IMPORT.UPLOAD_HELP_LINK' | translate}}
-
{{'CONNECTION_IMPORT.UPLOAD_DROP_TITLE' | translate}}
+
{{'IMPORT.UPLOAD_DROP_TITLE' | translate}}
- {{'CONNECTION_IMPORT.UPLOAD_BROWSE_LINK' | translate}} + {{'IMPORT.UPLOAD_BROWSE_LINK' | translate}}
{{fileName}}
diff --git a/guacamole/src/main/frontend/src/app/import/types/DisplayErrorList.js b/guacamole/src/main/frontend/src/app/import/types/DisplayErrorList.js new file mode 100644 index 000000000..d775c5734 --- /dev/null +++ b/guacamole/src/main/frontend/src/app/import/types/DisplayErrorList.js @@ -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 DisplayErrorList class. + */ +angular.module('import').factory('DisplayErrorList', [ + function defineDisplayErrorList() { + + /** + * A list of human-readable error messages, intended to be usable in a + * sortable / filterable table. + * + * @constructor + * @param {String[]} messages + * The error messages that should be prepared for display. + */ + const DisplayErrorList = function DisplayErrorList(messages) { + + // Use empty message list by default + this.messages = messages || []; + + // The single String message composed of all messages concatenated + // together. This will be used for filtering / sorting, and should only + // be calculated once. + this.cachedMessage = null; + + }; + + /** + * Return a sortable / filterable representation of all the error messages + * wrapped by this DisplayErrorList. + * + * NOTE: Once this method is called, any changes to the underlying array + * will have no effect. This is to ensure that repeated calls to toString() + * by sorting / filtering UI code will not regenerate the concatenated + * message every time. + * + * @returns {String} + * A sortable / filterable representation of the error messages wrapped + * by this DisplayErrorList + */ + DisplayErrorList.prototype.toString = function messageListToString() { + + // Generate the concatenated message if not already generated + if (!this.concatenatedMessage) + this.concatenatedMessage = this.messages.join(' '); + + return this.concatenatedMessage; + + } + + /** + * Return the underlying array containing the raw error messages, wrapped + * by this DisplayErrorList. + * + * @returns {String[]} + * The underlying array containing the raw error messages, wrapped by + * this DisplayErrorList + */ + DisplayErrorList.prototype.getArray = function getUnderlyingArray() { + return this.messages; + } + + return DisplayErrorList; + +}]); \ No newline at end of file diff --git a/guacamole/src/main/frontend/src/app/import/types/ImportConnection.js b/guacamole/src/main/frontend/src/app/import/types/ImportConnection.js index 9f336ef77..5639ba1c6 100644 --- a/guacamole/src/main/frontend/src/app/import/types/ImportConnection.js +++ b/guacamole/src/main/frontend/src/app/import/types/ImportConnection.js @@ -32,7 +32,7 @@ angular.module('import').factory('ImportConnection', [ * The object whose properties should be copied within the new * Connection. */ - var ImportConnection = function ImportConnection(template) { + const ImportConnection = function ImportConnection(template) { // Use empty object by default template = template || {}; diff --git a/guacamole/src/main/frontend/src/app/import/types/ImportConnectionError.js b/guacamole/src/main/frontend/src/app/import/types/ImportConnectionError.js new file mode 100644 index 000000000..4d4fd3661 --- /dev/null +++ b/guacamole/src/main/frontend/src/app/import/types/ImportConnectionError.js @@ -0,0 +1,108 @@ +/* + * 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 ImportConnectionError class. + */ +angular.module('import').factory('ImportConnectionError', ['$injector', + function defineImportConnectionError($injector) { + + // Required types + const DisplayErrorList = $injector.get('DisplayErrorList'); + + /** + * A representation of a connection to be imported, as parsed from an + * user-supplied import file. + * + * @constructor + * @param {ImportConnection|Object} [template={}] + * The object whose properties should be copied within the new + * Connection. + */ + const ImportConnectionError = function ImportConnectionError(template) { + + // Use empty object by default + template = template || {}; + + /** + * The row number within the original connection import file for this + * connection. This should be 1-indexed. + */ + this.rowNumber = template.rowNumber; + + /** + * The unique identifier of the connection group that contains this + * connection. + * + * @type String + */ + this.parentIdentifier = template.parentIdentifier; + + /** + * The path to the connection group that contains this connection, + * written as e.g. "ROOT/parent/child/group". + * + * @type String + */ + this.group = template.group; + + /** + * The human-readable name of this connection, which is not necessarily + * unique. + * + * @type String + */ + this.name = template.name; + + /** + * The name of the protocol associated with this connection, such as + * "vnc" or "rdp". + * + * @type String + */ + this.protocol = template.protocol; + + /** + * The identifiers of all users who should be granted read access to + * this connection. + * + * @type String[] + */ + this.users = template.users || []; + + /** + * The identifiers of all user groups who should be granted read access + * to this connection. + * + * @type String[] + */ + this.groups = template.groups || []; + + /** + * The error messages associated with this particular connection, if any. + * + * @type ImportConnectionError + */ + this.errors = template.errors || new DisplayErrorList(); + + }; + + return ImportConnectionError; + +}]); \ No newline at end of file diff --git a/guacamole/src/main/frontend/src/app/rest/services/connectionService.js b/guacamole/src/main/frontend/src/app/rest/services/connectionService.js index 5c1451ec2..c46874ea4 100644 --- a/guacamole/src/main/frontend/src/app/rest/services/connectionService.js +++ b/guacamole/src/main/frontend/src/app/rest/services/connectionService.js @@ -24,9 +24,11 @@ angular.module('rest').factory('connectionService', ['$injector', function connectionService($injector) { // Required services - var requestService = $injector.get('requestService'); var authenticationService = $injector.get('authenticationService'); var cacheService = $injector.get('cacheService'); + + // Required types + const Error = $injector.get('Error'); var service = {}; diff --git a/guacamole/src/main/frontend/src/app/rest/types/Error.js b/guacamole/src/main/frontend/src/app/rest/types/Error.js index 47f9cf770..74b28ebae 100644 --- a/guacamole/src/main/frontend/src/app/rest/types/Error.js +++ b/guacamole/src/main/frontend/src/app/rest/types/Error.js @@ -78,6 +78,15 @@ angular.module('rest').factory('Error', [function defineError() { */ this.expected = template.expected; + /** + * The outcome for each patch that was submitted as part of the request + * that generated this error, if the request was a directory PATCH + * request. In all other cases, this will be null. + * + * @type DirectoryPatchOutcome[] + */ + this.patches = template.patches || null; + }; /** diff --git a/guacamole/src/main/frontend/src/translations/en.json b/guacamole/src/main/frontend/src/translations/en.json index 0233f8bb6..c5292b80f 100644 --- a/guacamole/src/main/frontend/src/translations/en.json +++ b/guacamole/src/main/frontend/src/translations/en.json @@ -184,11 +184,13 @@ }, - "CONNECTION_IMPORT": { + "IMPORT": { "BUTTON_CANCEL": "Cancel", "BUTTON_IMPORT": "Import Connections", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "HEADER": "Connection Import", "HELP_HEADER": "Connection Import File Format", @@ -196,7 +198,6 @@ "HELP_FILE_TYPE_HEADER": "File Types", "HELP_FILE_TYPE_DESCRIPTION" : "Three file types are supported for connection import: CSV, JSON, and YAML. The same data may be specified by each file type. This must include the connection name and protocol. Optionally, a connection group location, a list of users and/or user groups to grant access, connection parameters, or connection protocols may also be specified. Any users or user groups that do not exist in the current data source will be automatically created.", - "HELP_CSV_HEADER": "CSV Format", "HELP_CSV_DESCRIPTION": "A connection import CSV file has one connection record per row. Each column will specify a connection field. At minimum the connection name and protocol must be specified.", "HELP_CSV_MORE_DETAILS": "The CSV header for each row specifies the connection field. The connection group ID that the connection should be imported into may be directly specified with \"parentIdentifier\", or the path to the parent group may be specified using \"group\" as shown below. In most cases, there should be no conflict between fields, but if needed, an \" (attribute)\" or \" (parameter)\" suffix may be added to disambiguate. Lists of user or user group identifiers must be semicolon-seperated.ยน", @@ -234,12 +235,17 @@ "ERROR_REQUIRED_NAME": "No connection name found in the provided file", + "ERROR_FILE_SINGLE_ONLY": "Please upload only a single file at a time", + + "TABLE_HEADER_NAME" : "Name", + "TABLE_HEADER_PROTOCOL" : "Protocol", + "TABLE_HEADER_ERRORS" : "Errors", + "TABLE_HEADER_ROW_NUMBER": "Row Number", + "UPLOAD_FILE_TYPES": "CSV, JSON, or YAML", "UPLOAD_HELP_LINK": "View Format Tips", "UPLOAD_DROP_TITLE": "Drop a File Here", - "UPLOAD_BROWSE_LINK": "Browse for File", - - "ERROR_FILE_SINGLE_ONLY": "Please upload only a single file at a time" + "UPLOAD_BROWSE_LINK": "Browse for File" },