mirror of
https://github.com/gyurix1968/guacamole-client.git
synced 2025-09-06 05:07:41 +00:00
GUACAMOLE-926: Display connection-specific errors to the user.
This commit is contained in:
@@ -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() {
|
||||
|
||||
|
@@ -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;
|
||||
|
||||
}]);
|
@@ -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;
|
||||
}
|
||||
|
||||
|
@@ -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']);
|
||||
|
@@ -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
|
||||
|
@@ -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 }
|
||||
});
|
||||
|
||||
|
@@ -29,3 +29,16 @@
|
||||
justify-content: center;
|
||||
|
||||
}
|
||||
|
||||
|
||||
.import .errors table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.import .errors .error-message {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.import .errors .error-message ul {
|
||||
margin: 0px;
|
||||
}
|
||||
|
@@ -0,0 +1,45 @@
|
||||
<div ng-show="hasErrors()" class="errors">
|
||||
|
||||
<!-- Connection / Error filter -->
|
||||
<guac-filter filtered-items="filteredErrors" items="connectionErrors"
|
||||
placeholder="'IMPORT.FIELD_PLACEHOLDER_FILTER' | translate"
|
||||
properties="filteredErrorProperties"></guac-filter>
|
||||
|
||||
<!-- List of current u -->
|
||||
<table class="sorted">
|
||||
<thead>
|
||||
<tr>
|
||||
<th guac-sort-order="errorOrder" guac-sort-property="'rowNumber'">
|
||||
{{'IMPORT.TABLE_HEADER_ROW_NUMBER' | translate}}
|
||||
</th>
|
||||
<th guac-sort-order="errorOrder" guac-sort-property="'name'">
|
||||
{{'IMPORT.TABLE_HEADER_NAME' | translate}}
|
||||
</th>
|
||||
<th guac-sort-order="errorOrder" guac-sort-property="'protocol'">
|
||||
{{'IMPORT.TABLE_HEADER_PROTOCOL' | translate}}
|
||||
</th>
|
||||
<th guac-sort-order="errorOrder" guac-sort-property="'errors'">
|
||||
{{'IMPORT.TABLE_HEADER_ERRORS' | translate}}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="error in errorPage">
|
||||
<td>{{error.rowNumber}}</td>
|
||||
<td>{{error.name}}</td>
|
||||
<td>{{error.protocol}}</td>
|
||||
<td class="error-message">
|
||||
<ul>
|
||||
<li ng-repeat="message in error.errors.getArray()">
|
||||
{{ message }}
|
||||
</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Pager for connection error list -->
|
||||
<guac-pager page="errorPage" page-size="25"
|
||||
items="filteredErrors | orderBy : errorOrder.predicate"></guac-pager>
|
||||
</div>
|
@@ -1,7 +1,7 @@
|
||||
<div class="settings-view import">
|
||||
|
||||
<div class="header">
|
||||
<h2>{{'CONNECTION_IMPORT.HEADER' | translate}}</h2>
|
||||
<h2>{{'IMPORT.HEADER' | translate}}</h2>
|
||||
<guac-user-menu></guac-user-menu>
|
||||
</div>
|
||||
|
||||
@@ -10,11 +10,11 @@
|
||||
<div class="import-buttons">
|
||||
<button
|
||||
ng-click="import()" ng-disabled="importDisabled()" class="import">
|
||||
{{'CONNECTION_IMPORT.BUTTON_IMPORT' | translate}}
|
||||
{{'IMPORT.BUTTON_IMPORT' | translate}}
|
||||
</button>
|
||||
<button
|
||||
ng-click="cancel()" ng-disabled="cancelDisabled()" class="cancel">
|
||||
{{'CONNECTION_IMPORT.BUTTON_CANCEL' | translate}}
|
||||
{{'IMPORT.BUTTON_CANCEL' | translate}}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -29,5 +29,8 @@
|
||||
{{error.message}}
|
||||
</p>
|
||||
|
||||
<!-- Connection specific errors, if there are any -->
|
||||
<connection-import-errors parse-result="parseResult" patch-failure="patchFailure" />
|
||||
|
||||
|
||||
</div>
|
||||
|
@@ -1,25 +1,25 @@
|
||||
<div class="import help">
|
||||
|
||||
<div class="header">
|
||||
<h2>{{'CONNECTION_IMPORT.HELP_HEADER' | translate}}</h2>
|
||||
<h2>{{'IMPORT.HELP_HEADER' | translate}}</h2>
|
||||
<guac-user-menu></guac-user-menu>
|
||||
</div>
|
||||
|
||||
<h2>{{'CONNECTION_IMPORT.HELP_FILE_TYPE_HEADER' | translate}}</h2>
|
||||
<p>{{'CONNECTION_IMPORT.HELP_FILE_TYPE_DESCRIPTION' | translate}}</p>
|
||||
<h2>{{'IMPORT.HELP_FILE_TYPE_HEADER' | translate}}</h2>
|
||||
<p>{{'IMPORT.HELP_FILE_TYPE_DESCRIPTION' | translate}}</p>
|
||||
|
||||
<h2>{{'CONNECTION_IMPORT.HELP_CSV_HEADER' | translate}}</h2>
|
||||
<p>{{'CONNECTION_IMPORT.HELP_CSV_DESCRIPTION' | translate}}</p>
|
||||
<p>{{'CONNECTION_IMPORT.HELP_CSV_MORE_DETAILS' | translate}}</p>
|
||||
<h2>{{'IMPORT.HELP_CSV_HEADER' | translate}}</h2>
|
||||
<p>{{'IMPORT.HELP_CSV_DESCRIPTION' | translate}}</p>
|
||||
<p>{{'IMPORT.HELP_CSV_MORE_DETAILS' | translate}}</p>
|
||||
<pre>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,,,,,</pre>
|
||||
|
||||
<h2>{{'CONNECTION_IMPORT.HELP_JSON_HEADER' | translate}}</h2>
|
||||
<p>{{'CONNECTION_IMPORT.HELP_JSON_DESCRIPTION' | translate}}</p>
|
||||
<p>{{'CONNECTION_IMPORT.HELP_JSON_MORE_DETAILS' | translate}}</p>
|
||||
<h2>{{'IMPORT.HELP_JSON_HEADER' | translate}}</h2>
|
||||
<p>{{'IMPORT.HELP_JSON_DESCRIPTION' | translate}}</p>
|
||||
<p>{{'IMPORT.HELP_JSON_MORE_DETAILS' | translate}}</p>
|
||||
<pre>[
|
||||
{
|
||||
"name": "conn1",
|
||||
@@ -51,8 +51,8 @@ conn4,kubernetes,,,,,</pre>
|
||||
}
|
||||
]</pre>
|
||||
|
||||
<h2>{{'CONNECTION_IMPORT.HELP_YAML_HEADER' | translate}}</h2>
|
||||
<p>{{'CONNECTION_IMPORT.HELP_YAML_DESCRIPTION' | translate}}</p>
|
||||
<h2>{{'IMPORT.HELP_YAML_HEADER' | translate}}</h2>
|
||||
<p>{{'IMPORT.HELP_YAML_DESCRIPTION' | translate}}</p>
|
||||
<pre>---
|
||||
- name: conn1
|
||||
protocol: vnc
|
||||
@@ -87,7 +87,7 @@ conn4,kubernetes,,,,,</pre>
|
||||
protocol: kubernetes</pre>
|
||||
|
||||
<ol class="footnotes">
|
||||
<li>{{'CONNECTION_IMPORT.HELP_SEMICOLON_FOOTNOTE' | translate}}</li>
|
||||
<li>{{'IMPORT.HELP_SEMICOLON_FOOTNOTE' | translate}}</li>
|
||||
</ol>
|
||||
|
||||
</div>
|
||||
|
@@ -1,20 +1,20 @@
|
||||
<div class="file-upload-container">
|
||||
|
||||
<div class="upload-header">
|
||||
<span class="file-options">{{'CONNECTION_IMPORT.UPLOAD_FILE_TYPES' | translate}}</span>
|
||||
<span class="file-options">{{'IMPORT.UPLOAD_FILE_TYPES' | translate}}</span>
|
||||
<a
|
||||
href="#/import/connection/file-format-help" target="_blank"
|
||||
class="file-help-link">{{'CONNECTION_IMPORT.UPLOAD_HELP_LINK' | translate}}
|
||||
class="file-help-link">{{'IMPORT.UPLOAD_HELP_LINK' | translate}}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="drop-target" ng-class="{ 'drop-pending': dropPending, 'file-present': fileName}">
|
||||
|
||||
<div ng-show="!fileName" class="title">{{'CONNECTION_IMPORT.UPLOAD_DROP_TITLE' | translate}}</div>
|
||||
<div ng-show="!fileName" class="title">{{'IMPORT.UPLOAD_DROP_TITLE' | translate}}</div>
|
||||
|
||||
<input type="file" class="file-upload-input"/>
|
||||
<a ng-show="!fileName" ng-click="openFileBrowser()" class="browse-link">
|
||||
{{'CONNECTION_IMPORT.UPLOAD_BROWSE_LINK' | translate}}
|
||||
{{'IMPORT.UPLOAD_BROWSE_LINK' | translate}}
|
||||
</a>
|
||||
|
||||
<div ng-show="fileName" class="file-name"> {{fileName}} </div>
|
||||
|
@@ -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;
|
||||
|
||||
}]);
|
@@ -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 || {};
|
||||
|
@@ -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;
|
||||
|
||||
}]);
|
@@ -24,10 +24,12 @@ 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 = {};
|
||||
|
||||
/**
|
||||
|
@@ -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;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
|
@@ -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"
|
||||
|
||||
},
|
||||
|
||||
|
Reference in New Issue
Block a user