mirror of
https://github.com/gyurix1968/guacamole-client.git
synced 2025-09-06 05:07:41 +00:00
GUACAMOLE-926: Parse YAML, CSV, JSON on frontend and submit to API.
This commit is contained in:
3907
guacamole/src/main/frontend/package-lock.json
generated
3907
guacamole/src/main/frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -12,13 +12,18 @@
|
||||
"angular-translate-interpolation-messageformat": "^2.19.0",
|
||||
"angular-translate-loader-static-files": "^2.19.0",
|
||||
"blob-polyfill": ">=7.0.20220408",
|
||||
"csv": "^6.2.5",
|
||||
"datalist-polyfill": "^1.25.1",
|
||||
"file-saver": "^2.0.5",
|
||||
"jquery": "^3.6.4",
|
||||
"jstz": "^2.1.1",
|
||||
"lodash": "^4.17.21"
|
||||
"lodash": "^4.17.21",
|
||||
"yaml": "^2.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.20.12",
|
||||
"@babel/preset-env": "^7.20.2",
|
||||
"babel-loader": "^8.3.0",
|
||||
"clean-webpack-plugin": "^4.0.0",
|
||||
"closure-webpack-plugin": "^2.6.1",
|
||||
"copy-webpack-plugin": "^5.1.2",
|
||||
|
@@ -126,6 +126,15 @@ angular.module('index').config(['$routeProvider', '$locationProvider',
|
||||
resolve : { routeToUserHomePage: routeToUserHomePage }
|
||||
})
|
||||
|
||||
// Connection import page
|
||||
.when('/settings/:dataSource/import', {
|
||||
title : 'APP.NAME',
|
||||
bodyClassName : 'settings',
|
||||
templateUrl : 'app/settings/templates/settingsImport.html',
|
||||
controller : 'importConnectionsController',
|
||||
resolve : { updateCurrentToken: updateCurrentToken }
|
||||
})
|
||||
|
||||
// Management screen
|
||||
.when('/settings/:dataSource?/:tab', {
|
||||
title : 'APP.NAME',
|
||||
|
@@ -154,6 +154,46 @@ angular.module('rest').factory('connectionService', ['$injector',
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Makes a request to the REST API to create multiple connections, returning a
|
||||
* a promise that can be used for processing the results of the call. This
|
||||
* operation is atomic - if any errors are encountered during the connection
|
||||
* creation process, the entire request will fail, and no connections will be
|
||||
* created.
|
||||
*
|
||||
* @param {Connection[]} connections The connections to create.
|
||||
*
|
||||
* @returns {Promise}
|
||||
* A promise for the HTTP call which will succeed if and only if the
|
||||
* create operation is successful.
|
||||
*/
|
||||
service.createConnections = function createConnections(dataSource, connections) {
|
||||
|
||||
// An object containing a PATCH operation to create each connection
|
||||
const patchBody = connections.map(connection => ({
|
||||
op: "add",
|
||||
path: "/",
|
||||
value: connection
|
||||
}));
|
||||
|
||||
// Make a PATCH request to create the connections
|
||||
return authenticationService.request({
|
||||
method : 'PATCH',
|
||||
url : 'api/session/data/' + encodeURIComponent(dataSource) + '/connections',
|
||||
data : patchBody
|
||||
})
|
||||
|
||||
// Clear the cache
|
||||
.then(function connectionUpdated(){
|
||||
cacheService.connections.removeAll();
|
||||
|
||||
// Clear users cache to force reload of permissions for this
|
||||
// newly updated connection
|
||||
cacheService.users.removeAll();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a request to the REST API to delete a connection,
|
||||
|
@@ -82,4 +82,4 @@ angular.module('rest').factory('RelatedObjectPatch', [function defineRelatedObje
|
||||
|
||||
return RelatedObjectPatch;
|
||||
|
||||
}]);
|
||||
}]);
|
||||
|
@@ -20,7 +20,7 @@
|
||||
/**
|
||||
* The controller for the session recording player page.
|
||||
*/
|
||||
angular.module('manage').controller('connectionHistoryPlayerController', ['$scope', '$injector',
|
||||
angular.module('settings').controller('connectionHistoryPlayerController', ['$scope', '$injector',
|
||||
function connectionHistoryPlayerController($scope, $injector) {
|
||||
|
||||
// Required services
|
||||
|
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The controller for the connection import page.
|
||||
*/
|
||||
angular.module('settings').controller('importConnectionsController', ['$scope', '$injector',
|
||||
function importConnectionsController($scope, $injector) {
|
||||
|
||||
// Required services
|
||||
const connectionImportParseService = $injector.get('connectionImportParseService');
|
||||
const connectionService = $injector.get('connectionService');
|
||||
|
||||
function processData(type, data) {
|
||||
|
||||
let requestBody;
|
||||
|
||||
// Parse the data based on the provided mimetype
|
||||
switch(type) {
|
||||
|
||||
case "application/json":
|
||||
case "text/json":
|
||||
requestBody = connectionImportParseService.parseJSON(data);
|
||||
break;
|
||||
|
||||
case "text/csv":
|
||||
requestBody = connectionImportParseService.parseCSV(data);
|
||||
break;
|
||||
|
||||
case "application/yaml":
|
||||
case "application/x-yaml":
|
||||
case "text/yaml":
|
||||
case "text/x-yaml":
|
||||
requestBody = connectionImportParseService.parseYAML(data);
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
console.log(requestBody);
|
||||
}
|
||||
|
||||
$scope.upload = function() {
|
||||
|
||||
const files = angular.element('#file')[0].files;
|
||||
|
||||
if (files.length <= 0) {
|
||||
console.error("TODO: This should be a proper error tho");
|
||||
return;
|
||||
}
|
||||
|
||||
// The file that the user uploaded
|
||||
const file = files[0];
|
||||
|
||||
// Call processData when the data is ready
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = (e => processData(file.type, e.target.result));
|
||||
|
||||
// Read all the data into memory and call processData when done
|
||||
reader.readAsBinaryString(file);
|
||||
}
|
||||
|
||||
}]);
|
@@ -20,7 +20,7 @@
|
||||
/**
|
||||
* The controller for the general settings page.
|
||||
*/
|
||||
angular.module('manage').controller('settingsController', ['$scope', '$injector',
|
||||
angular.module('settings').controller('settingsController', ['$scope', '$injector',
|
||||
function settingsController($scope, $injector) {
|
||||
|
||||
// Required services
|
||||
|
@@ -0,0 +1,355 @@
|
||||
/*
|
||||
* 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 _ */
|
||||
|
||||
import { parse as parseCSVData } from 'csv-parse/lib/sync'
|
||||
import { parse as parseYAMLData } from 'yaml'
|
||||
|
||||
/**
|
||||
* A service for parsing user-provided JSON, YAML, or JSON connection data into
|
||||
* an appropriate format for bulk uploading using the PATCH REST endpoint.
|
||||
*/
|
||||
angular.module('settings').factory('connectionImportParseService',
|
||||
['$injector', function connectionImportParseService($injector) {
|
||||
|
||||
// Required types
|
||||
const Connection = $injector.get('Connection');
|
||||
const ParseError = $injector.get('ParseError');
|
||||
const TranslatableMessage = $injector.get('TranslatableMessage');
|
||||
|
||||
// Required services
|
||||
const $q = $injector.get('$q');
|
||||
const $routeParams = $injector.get('$routeParams');
|
||||
const schemaService = $injector.get('schemaService');
|
||||
const connectionGroupService = $injector.get('connectionGroupService');
|
||||
|
||||
const service = {};
|
||||
|
||||
/**
|
||||
* Perform basic checks, common to all file types - namely that the parsed
|
||||
* data is an array, and contains at least one connection entry.
|
||||
*
|
||||
* @throws {ParseError}
|
||||
* An error describing the parsing failure, if one of the basic checks
|
||||
* fails.
|
||||
*/
|
||||
function performBasicChecks(parsedData) {
|
||||
|
||||
// Make sure that the file data parses to an array (connection list)
|
||||
if (!(parsedData instanceof Array))
|
||||
throw new ParseError({
|
||||
message: 'Import data must be a list of connections',
|
||||
translatableMessage: new TranslatableMessage({
|
||||
key: 'SETTINGS_CONNECTION_IMPORT.ERROR_ARRAY_REQUIRED'
|
||||
})
|
||||
});
|
||||
|
||||
// Make sure that the connection list is not empty - contains at least
|
||||
// one connection
|
||||
if (!parsedData.length)
|
||||
throw new ParseError({
|
||||
message: 'The provided CSV file is empty',
|
||||
translatableMessage: new TranslatableMessage({
|
||||
key: 'SETTINGS_CONNECTION_IMPORT.ERROR_EMPTY_FILE'
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* 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 a promise that resolves to an object mapping potential groups
|
||||
* that might be encountered in an imported connection to group identifiers.
|
||||
*
|
||||
* The idea is that a user-provided import file might directly specify a
|
||||
* parentIdentifier, or it might specify a named group path like "ROOT",
|
||||
* "ROOT/parent", or "ROOT/parent/child". This object resolved by the
|
||||
* promise returned from this function will map all of the above to the
|
||||
* identifier of the appropriate group, if defined.
|
||||
*
|
||||
* @returns {Promise.<Object>}
|
||||
*/
|
||||
function getGroupLookups() {
|
||||
|
||||
// The current data source - defines all the groups that the connections
|
||||
// might be imported into
|
||||
const dataSource = $routeParams.dataSource;
|
||||
|
||||
const deferredGroupLookups = $q.defer();
|
||||
|
||||
connectionGroupService.getConnectionGroupTree(dataSource).then(
|
||||
rootGroup => {
|
||||
|
||||
const groupLookup = {};
|
||||
|
||||
// Add the specified group to the lookup, appending all specified
|
||||
// prefixes, and then recursively call saveLookups for all children
|
||||
// of the group, appending to the prefix for each level
|
||||
function saveLookups(prefix, group) {
|
||||
|
||||
// To get the path for the current group, add the name
|
||||
const currentPath = prefix + group.name;
|
||||
|
||||
// Add the current path to the lookup
|
||||
groupLookup[currentPath] = group.identifier;
|
||||
|
||||
// Add each child group to the lookup
|
||||
const nextPrefix = currentPath + "/";
|
||||
_.forEach(group.childConnectionGroups,
|
||||
childGroup => saveLookups(nextPrefix, childGroup));
|
||||
}
|
||||
|
||||
// Start at the root group
|
||||
saveLookups("", rootGroup);
|
||||
|
||||
// Resolve with the now fully-populated lookups
|
||||
deferredGroupLookups.resolve(groupLookup);
|
||||
|
||||
});
|
||||
|
||||
return deferredGroupLookups.promise;
|
||||
}
|
||||
|
||||
/*
|
||||
// Example Connection JSON
|
||||
{
|
||||
"attributes": {
|
||||
"failover-only": "true",
|
||||
"guacd-encryption": "none",
|
||||
"guacd-hostname": "potato",
|
||||
"guacd-port": "1234",
|
||||
"ksm-user-config-enabled": "true",
|
||||
"max-connections": "1",
|
||||
"max-connections-per-user": "1",
|
||||
"weight": "1"
|
||||
},
|
||||
"name": "Bloatato",
|
||||
"parameters": {
|
||||
"audio-servername": "heyoooooooo",
|
||||
"clipboard-encoding": "",
|
||||
"color-depth": "",
|
||||
"create-recording-path": "",
|
||||
"cursor": "remote",
|
||||
"dest-host": "pooootato",
|
||||
"dest-port": "4444",
|
||||
"disable-copy": "",
|
||||
"disable-paste": "true",
|
||||
"enable-audio": "true",
|
||||
"enable-sftp": "true",
|
||||
"force-lossless": "true",
|
||||
"hostname": "potato",
|
||||
"password": "taste",
|
||||
"port": "4321",
|
||||
"read-only": "",
|
||||
"recording-exclude-mouse": "",
|
||||
"recording-exclude-output": "",
|
||||
"recording-include-keys": "",
|
||||
"recording-name": "heyoooooo",
|
||||
"recording-path": "/path/to/goo",
|
||||
"sftp-disable-download": "",
|
||||
"sftp-disable-upload": "",
|
||||
"sftp-hostname": "what what good sir",
|
||||
"sftp-port": "",
|
||||
"sftp-private-key": "lol i'll never tell",
|
||||
"sftp-server-alive-interval": "",
|
||||
"swap-red-blue": "true",
|
||||
"username": "test",
|
||||
"wol-send-packet": "",
|
||||
"wol-udp-port": "",
|
||||
"wol-wait-time": ""
|
||||
},
|
||||
|
||||
// or a numeric identifier - we will probably want to offer a way to allow
|
||||
// them to specify a path like "ROOT/parent/child" or just "/parent/child" or
|
||||
// something like that
|
||||
// TODO: Call the
|
||||
"parentIdentifier": "ROOT",
|
||||
"protocol": "vnc"
|
||||
|
||||
}
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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 will contain a PATCH operation to create each connection in the
|
||||
* provided list.
|
||||
*
|
||||
* TODO: Describe disambiguation suffixes, e.g. hostname (parameter), and
|
||||
* that we will accept without the suffix if it's unambigous. (or not? how about not?)
|
||||
*
|
||||
* @param {String} csvData
|
||||
* The JSON-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.parseCSV = function parseCSV(csvData) {
|
||||
|
||||
const deferredConnections = $q.defer();
|
||||
|
||||
return $q.all({
|
||||
fieldLookups : getFieldLookups(),
|
||||
groupLookups : getGroupLookups()
|
||||
})
|
||||
.then(function lookupsReady({fieldLookups, groupLookups}) {
|
||||
|
||||
const {attributes, protocolParameters} = fieldLookups;
|
||||
|
||||
console.log({attributes, protocolParameters}, groupLookups);
|
||||
|
||||
// Convert to an array of arrays, one per CSV row (including the header)
|
||||
const parsedData = parseCSVData(csvData);
|
||||
|
||||
// Slice off the header row to get the data rows
|
||||
const connectionData = parsedData.slice(1);
|
||||
|
||||
// Check that the provided CSV is not empty (the parser always
|
||||
// returns an array)
|
||||
performBasicChecks(connectionData);
|
||||
|
||||
// The header row - an array of string header values
|
||||
const header = parsedData[0];
|
||||
|
||||
// TODO: Connectionify this
|
||||
deferredConnections.resolve(connectionData);
|
||||
});
|
||||
|
||||
return deferredConnections.promise;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 will contain a PATCH operation to create each connection in the
|
||||
* provided list.
|
||||
*
|
||||
* @param {String} jsonData
|
||||
* The JSON-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.parseJSON = function parseJSON(jsonData) {
|
||||
|
||||
// Parse from JSON into a javascript array
|
||||
const parsedData = JSON.parse(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;
|
||||
|
||||
};
|
||||
|
||||
return service;
|
||||
|
||||
}]);
|
@@ -20,7 +20,8 @@
|
||||
a.button.add-user,
|
||||
a.button.add-user-group,
|
||||
a.button.add-connection,
|
||||
a.button.add-connection-group {
|
||||
a.button.add-connection-group,
|
||||
a.button.import-connections {
|
||||
font-size: 0.8em;
|
||||
padding-left: 1.8em;
|
||||
position: relative;
|
||||
@@ -29,7 +30,8 @@ a.button.add-connection-group {
|
||||
a.button.add-user::before,
|
||||
a.button.add-user-group::before,
|
||||
a.button.add-connection::before,
|
||||
a.button.add-connection-group::before {
|
||||
a.button.add-connection-group::before,
|
||||
a.button.import-connections::before {
|
||||
|
||||
content: ' ';
|
||||
position: absolute;
|
||||
@@ -59,3 +61,7 @@ a.button.add-connection::before {
|
||||
a.button.add-connection-group::before {
|
||||
background-image: url('images/action-icons/guac-group-add.svg');
|
||||
}
|
||||
|
||||
a.button.import-connections::before {
|
||||
background-image: url('images/action-icons/guac-monitor-add-many.svg');
|
||||
}
|
||||
|
@@ -9,6 +9,10 @@
|
||||
<!-- Form action buttons -->
|
||||
<div class="action-buttons">
|
||||
|
||||
<a class="import-connections button"
|
||||
ng-show="canCreateConnections()"
|
||||
href="#/settings/{{dataSource | escape}}/import/">{{'SETTINGS_CONNECTIONS.ACTION_IMPORT_CONNECTIONS' | translate}}</a>
|
||||
|
||||
<a class="add-connection button"
|
||||
ng-show="canCreateConnections()"
|
||||
href="#/manage/{{dataSource | escape}}/connections/">{{'SETTINGS_CONNECTIONS.ACTION_NEW_CONNECTION' | translate}}</a>
|
||||
|
@@ -0,0 +1,11 @@
|
||||
<div class="settings-view import">
|
||||
|
||||
<div class="header">
|
||||
<h2>{{'SETTINGS_CONNECTION_IMPORT.HEADER' | translate}}</h2>
|
||||
<guac-user-menu></guac-user-menu>
|
||||
</div>
|
||||
|
||||
<input type="file" id="file" name="file"/>
|
||||
<button ng-click="upload()">Add</button>
|
||||
|
||||
</div>
|
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* 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 ParseError class.
|
||||
*/
|
||||
angular.module('settings').factory('ParseError', [function defineParseError() {
|
||||
|
||||
/**
|
||||
* An error representing a parsing failure when attempting to convert
|
||||
* user-provided data into a list of Connection objects.
|
||||
*
|
||||
* @constructor
|
||||
* @param {ParseError|Object} [template={}]
|
||||
* The object whose properties should be copied within the new
|
||||
* ParseError.
|
||||
*/
|
||||
var ParseError = function ParseError(template) {
|
||||
|
||||
// Use empty object by default
|
||||
template = template || {};
|
||||
|
||||
/**
|
||||
* A human-readable message describing the error that occurred.
|
||||
*
|
||||
* @type String
|
||||
*/
|
||||
this.message = template.message;
|
||||
|
||||
/**
|
||||
* A message which can be translated using the translation service,
|
||||
* consisting of a translation key and optional set of substitution
|
||||
* variables.
|
||||
*
|
||||
* @type TranslatableMessage
|
||||
*/
|
||||
this.translatableMessage = template.translatableMessage;
|
||||
|
||||
};
|
||||
|
||||
return ParseError;
|
||||
|
||||
}]);
|
@@ -0,0 +1,77 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="64"
|
||||
height="64"
|
||||
viewBox="0 0 64 64"
|
||||
version="1.1"
|
||||
id="svg3828"
|
||||
sodipodi:docname="guac-monitor-add-many.svg"
|
||||
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs3832" />
|
||||
<sodipodi:namedview
|
||||
id="namedview3830"
|
||||
pagecolor="#5b5b5b"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pagecheckerboard="true"
|
||||
showgrid="false"
|
||||
inkscape:zoom="6.546875"
|
||||
inkscape:cx="33.603819"
|
||||
inkscape:cy="22.377088"
|
||||
inkscape:window-width="2048"
|
||||
inkscape:window-height="1025"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="28"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg3828" />
|
||||
<g
|
||||
transform="translate(-19.073 1.734) scale(.10947)"
|
||||
style="fill:#fff"
|
||||
id="g3822">
|
||||
<path
|
||||
style="fill:#fff;fill-opacity:1;stroke:#000;stroke-width:.00956892;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0"
|
||||
d="M4 .656c-2.215 0-4 1.927-4 4.313V48.03c0 2.386 1.785 4.282 4 4.282h56c2.215 0 4-1.896 4-4.282V4.97c0-2.386-1.785-4.313-4-4.313H4zm10.313 10.938h35.374c1.4 0 2.532 1.093 2.532 2.469v24.843c0 1.376-1.132 2.469-2.532 2.469H14.313c-1.4 0-2.532-1.093-2.532-2.469V14.063c0-1.376 1.132-2.47 2.531-2.47z"
|
||||
transform="translate(174.22 231.813) scale(5.22525)"
|
||||
id="path3818" />
|
||||
<rect
|
||||
ry="18"
|
||||
rx="18"
|
||||
y="524.187"
|
||||
x="244.268"
|
||||
height="38.636"
|
||||
width="197.179"
|
||||
style="fill:#fff;fill-opacity:1;stroke:#000;stroke-width:.05000001;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0"
|
||||
id="rect3820" />
|
||||
</g>
|
||||
<path
|
||||
style="fill:#ffffff;stroke:none"
|
||||
d="m 48.309,0.865 h 7.518 V 23 h -7.518 z"
|
||||
id="path3824" />
|
||||
<path
|
||||
style="fill:#ffffff;stroke:none"
|
||||
d="m 63.135,8.173 v 7.518 H 41 V 8.173 Z"
|
||||
id="path3826" />
|
||||
<path
|
||||
style="fill:#ffffff;stroke:none"
|
||||
d="m 48.309,27 h 7.518 v 22.135 h -7.518 z"
|
||||
id="path3824-2" />
|
||||
<path
|
||||
style="fill:#ffffff;stroke:none"
|
||||
d="m 63.135,34.308 v 7.518 H 41 v -7.518 z"
|
||||
id="path3826-8" />
|
||||
<path
|
||||
style="fill:#ffffff;stroke:none"
|
||||
d="m 22.174,0.865 h 7.518 V 23 h -7.518 z"
|
||||
id="path3824-4" />
|
||||
<path
|
||||
style="fill:#ffffff;stroke:none"
|
||||
d="m 37,8.173 v 7.518 H 14.865 V 8.173 Z"
|
||||
id="path3826-7" />
|
||||
</svg>
|
After Width: | Height: | Size: 2.7 KiB |
@@ -5,7 +5,7 @@
|
||||
"APP" : {
|
||||
|
||||
"NAME" : "Apache Guacamole",
|
||||
"VERSION" : "${project.version}",
|
||||
"VERSION" : "1.5.0",
|
||||
|
||||
"ACTION_ACKNOWLEDGE" : "OK",
|
||||
"ACTION_CANCEL" : "Cancel",
|
||||
@@ -906,6 +906,7 @@
|
||||
"SETTINGS_CONNECTIONS" : {
|
||||
|
||||
"ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE",
|
||||
"ACTION_IMPORT_CONNECTIONS" : "Import",
|
||||
"ACTION_NEW_CONNECTION" : "New Connection",
|
||||
"ACTION_NEW_CONNECTION_GROUP" : "New Group",
|
||||
"ACTION_NEW_SHARING_PROFILE" : "New Sharing Profile",
|
||||
@@ -922,6 +923,13 @@
|
||||
|
||||
},
|
||||
|
||||
"SETTINGS_CONNECTION_IMPORT": {
|
||||
|
||||
"HEADER": "Connection Import",
|
||||
|
||||
"ERROR_ARRAY_REQUIRED": "The provided file must contain a list of connections"
|
||||
},
|
||||
|
||||
"SETTINGS_PREFERENCES" : {
|
||||
|
||||
"ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE",
|
||||
|
@@ -47,6 +47,22 @@ module.exports = {
|
||||
module: {
|
||||
rules: [
|
||||
|
||||
// NOTE: This is required in order to parse ES2020 language features,
|
||||
// like the optional chaining and nullish coalescing operators. It
|
||||
// specifically needs to operate on the node-modules directory since
|
||||
// Webpack 4 cannot handle such language features.
|
||||
{
|
||||
test: /\.js$/i,
|
||||
use: {
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
presets: [
|
||||
['@babel/preset-env']
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Automatically extract imported CSS for later reference within separate CSS file
|
||||
{
|
||||
test: /\.css$/i,
|
||||
|
Reference in New Issue
Block a user