From 2ce7876f26845502a2d5b73f616ab7b8b546f867 Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Fri, 3 Mar 2023 02:38:47 +0000 Subject: [PATCH] GUACAMOLE-926: Migrate upload directive into import page controller since they're so tangled up together to make it not worth seperating them. --- .../importConnectionsController.js | 178 ++++++++++++- .../directives/connectionImportFileUpload.js | 252 ------------------ .../src/app/import/styles/file-upload.css | 119 --------- .../frontend/src/app/import/styles/import.css | 101 +++++++ .../import/templates/connectionImport.html | 26 +- .../templates/connectionImportFileUpload.html | 30 --- 6 files changed, 300 insertions(+), 406 deletions(-) delete mode 100644 guacamole/src/main/frontend/src/app/import/directives/connectionImportFileUpload.js delete mode 100644 guacamole/src/main/frontend/src/app/import/styles/file-upload.css delete mode 100644 guacamole/src/main/frontend/src/app/import/templates/connectionImportFileUpload.html 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 ec1227e95..14da850f9 100644 --- a/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js +++ b/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js @@ -25,9 +25,14 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$injector', function importConnectionsController($scope, $injector) { + // The file types supported for connection import + const LEGAL_FILE_TYPES = ['csv', 'json', 'yaml']; + // Required services + const $document = $injector.get('$document'); const $q = $injector.get('$q'); const $routeParams = $injector.get('$routeParams'); + const $timeout = $injector.get('$timeout'); const connectionParseService = $injector.get('connectionParseService'); const connectionService = $injector.get('connectionService'); const permissionService = $injector.get('permissionService'); @@ -112,11 +117,13 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ $scope.aborted = false; $scope.dataReady = false; $scope.processing = false; + $scope.error = null; $scope.fileData = null; $scope.mimeType = null; $scope.fileReader = null; $scope.parseResult = null; $scope.patchFailure = null; + $scope.fileName = null; // Broadcast an event to clear the file upload UI $scope.$broadcast('clearFile'); @@ -410,7 +417,6 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ resetUploadState(); // Set the error for display - console.error(error); $scope.error = error; }; @@ -462,8 +468,6 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ } // Make the call to process the data into a series of patches - // TODO: Check if there's errors, and if so, display those rather than - // just YOLOing a create call processDataCallback(data) // Send the data off to be imported if parsing is successful @@ -537,7 +541,30 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ * @argument {File} file * The file to upload onto the scope for further processing. */ - $scope.handleFile = function(file) { + const handleFile = file => { + + // Clear any error from a previous attempted file upload + clearError(); + + // The MIME type of the provided file + const mimeType = file.type; + + // Check if the mimetype ends with one of the supported types, + // e.g. "application/json" or "text/csv" + if (_.every(LEGAL_FILE_TYPES.map( + type => !mimeType.endsWith(type)))) { + + // If the provided file is not one of the supported types, + // display an error and abort processing + handleError(new ParseError({ + message: "Invalid file type: " + type, + key: 'IMPORT.ERROR_INVALID_FILE_TYPE', + variables: { TYPE: mimeType } + })); + return; + } + + $scope.fileName = file.name; // Clear any error message from the previous upload attempt clearError(); @@ -578,5 +605,148 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ // Read all the data into memory $scope.fileReader.readAsBinaryString(file); }; + + /** + * Whether a drag/drop operation is currently in progress (the user has + * dragged a file over the Guacamole connection but has not yet + * dropped it). + * + * @type boolean + */ + $scope.dropPending = false; + + /** + * The name of the file that's currently being uploaded, or has yet to + * be imported, if any. + */ + $scope.fileName = null; + + /** + * The container for the file upload UI. + * + * @type Element + * + */ + const uploadContainer = angular.element( + $document.find('.file-upload-container')); + + /** + * The location where files can be dragged-and-dropped to. + * + * @type Element + */ + const dropTarget = uploadContainer.find('.drop-target'); + + /** + * Displays a visual indication that dropping the file currently + * being dragged is possible. Further propagation and default behavior + * of the given event is automatically prevented. + * + * @param {Event} e + * The event related to the in-progress drag/drop operation. + */ + const notifyDragStart = function notifyDragStart(e) { + + e.preventDefault(); + e.stopPropagation(); + + $scope.$apply(() => { + $scope.dropPending = true; + }); + + }; + + /** + * Removes the visual indication that dropping the file currently + * being dragged is possible. Further propagation and default behavior + * of the given event is automatically prevented. + * + * @param {Event} e + * The event related to the end of the former drag/drop operation. + */ + const notifyDragEnd = function notifyDragEnd(e) { + + e.preventDefault(); + e.stopPropagation(); + + $scope.$apply(() => { + $scope.dropPending = false; + }); + + }; + + // Add listeners to the drop target to ensure that the visual state + // stays up to date + dropTarget.on('dragenter', notifyDragStart); + dropTarget.on('dragover', notifyDragStart); + dropTarget.on('dragleave', notifyDragEnd); + + /** + * Drop target event listener that will be invoked if the user drops + * anything onto the drop target. If a valid file is provided, the + * onFile callback provided to this directive will be called; otherwise + * an error will be displayed, if appropriate. + * + * @param {Event} e + * The drop event that triggered this handler. + */ + dropTarget.on('drop', e => { + + notifyDragEnd(e); + + const files = e.originalEvent.dataTransfer.files; + + // Ignore any non-files that are dragged into the drop area + if (files.length < 1) + return; + + if (files.length >= 2) { + + // If more than one file was provided, print an error explaining + // that only a single file is allowed and abort processing + handleError(new ParseError({ + message: 'Only a single file may be imported at once', + key: 'IMPORT.ERROR_FILE_SINGLE_ONLY' + })); + return; + } + + handleFile(files[0]); + + }); + + /** + * The hidden file input used to create a file browser. + * + * @type Element + */ + const fileUploadInput = uploadContainer.find('.file-upload-input'); + + /** + * A function that will click on the hidden file input to open a file + * browser to allow the user to select a file for upload. + */ + $scope.openFileBrowser = () => + $timeout(() => fileUploadInput.click(), 0, false); + + /** + * A handler that will be invoked when a user selectes a file in the + * file browser. After some error checking, the file will be passed to + * the onFile callback provided to this directive. + * + * @param {Event} e + * The event that was triggered when the user selected a file in + * their file browser. + */ + fileUploadInput.on('change', e => { + + // Process the uploaded file + handleFile(e.target.files[0]); + + // Clear the value to ensure that the change event will be fired + // if the user selects the same file again + fileUploadInput.value = null; + + }); }]); diff --git a/guacamole/src/main/frontend/src/app/import/directives/connectionImportFileUpload.js b/guacamole/src/main/frontend/src/app/import/directives/connectionImportFileUpload.js deleted file mode 100644 index 196e8f48f..000000000 --- a/guacamole/src/main/frontend/src/app/import/directives/connectionImportFileUpload.js +++ /dev/null @@ -1,252 +0,0 @@ -/* - * 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 _ */ - -/** - * All legal import file types. Any file not belonging to one of these types - * must be rejected. - */ -const LEGAL_FILE_TYPES = ["csv", "json", "yaml"]; - -/** - * A directive that allows for file upload, either through drag-and-drop or - * a file browser. - */ -angular.module('import').directive('connectionImportFileUpload', [ - function connectionImportFileUpload() { - - const directive = { - restrict: 'E', - replace: true, - templateUrl: 'app/import/templates/connectionImportFileUpload.html', - scope: { - - /** - * The function to invoke when a file is provided to the file upload - * UI, either by dragging and dropping, or by navigating using the - * file browser. The function will be called with 2 arguments - the - * mime type, and the raw string contents of the file. - * - * @type function - */ - onFile : '&', - } - }; - - directive.controller = ['$scope', '$injector', '$element', - function fileUploadController($scope, $injector, $element) { - - // Required services - const $timeout = $injector.get('$timeout'); - - /** - * Whether a drag/drop operation is currently in progress (the user has - * dragged a file over the Guacamole connection but has not yet - * dropped it). - * - * @type boolean - */ - $scope.dropPending = false; - - /** - * The error associated with the file upload, if any. An object of the - * form { key, variables }, or null if no error has occured. - */ - $scope.error = null; - - /** - * The name of the file that's currently being uploaded, or has yet to - * be imported, if any. - */ - $scope.fileName = null; - - // Clear the file if instructed to do so by the parent - $scope.$on('clearFile', () => delete $scope.fileName); - - /** - * Clear any displayed error message. - */ - const clearError = () => $scope.error = null; - - /** - * Set an error for display using the provided translation key and - * translation variables. - * - * @param {String} key - * The translation key. - * - * @param {Object.} variables - * The variables to subsitute into the message, if any. - */ - const setError = (key, variables) => $scope.error = { key, variables }; - - /** - * The location where files can be dragged-and-dropped to. - * - * @type Element - */ - const dropTarget = $element.find('.drop-target')[0]; - - /** - * Displays a visual indication that dropping the file currently - * being dragged is possible. Further propagation and default behavior - * of the given event is automatically prevented. - * - * @param {Event} e - * The event related to the in-progress drag/drop operation. - */ - const notifyDragStart = function notifyDragStart(e) { - - e.preventDefault(); - e.stopPropagation(); - - $scope.$apply(() => { - $scope.dropPending = true; - }); - - }; - - /** - * Removes the visual indication that dropping the file currently - * being dragged is possible. Further propagation and default behavior - * of the given event is automatically prevented. - * - * @param {Event} e - * The event related to the end of the former drag/drop operation. - */ - const notifyDragEnd = function notifyDragEnd(e) { - - e.preventDefault(); - e.stopPropagation(); - - $scope.$apply(() => { - $scope.dropPending = false; - }); - - }; - - // Add listeners to the drop target to ensure that the visual state - // stays up to date - dropTarget.addEventListener('dragenter', notifyDragStart, false); - dropTarget.addEventListener('dragover', notifyDragStart, false); - dropTarget.addEventListener('dragleave', notifyDragEnd, false); - - /** - * Given a user-supplied file, validate that the file type is correct, - * and invoke the onFile callback provided to this directive if so. - * - * @param {File} file - * The user-supplied file. - */ - function handleFile(file) { - - // Clear any error from a previous attempted file upload - clearError(); - - // The MIME type of the provided file - const mimeType = file.type; - - // Check if the mimetype ends with one of the supported types, - // e.g. "application/json" or "text/csv" - if (_.every(LEGAL_FILE_TYPES.map( - type => !mimeType.endsWith(type)))) { - - // If the provided file is not one of the supported types, - // display an error and abort processing - setError('IMPORT.ERROR_INVALID_FILE_TYPE', - { TYPE: mimeType }); - return; - } - - $scope.fileName = file.name; - - // Invoke the provided file callback using the file - $scope.onFile({ file }); - } - - /** - * Drop target event listener that will be invoked if the user drops - * anything onto the drop target. If a valid file is provided, the - * onFile callback provided to this directive will be called; otherwise - * an error will be displayed, if appropriate. - * - * @param {Event} e - * The drop event that triggered this handler. - */ - dropTarget.addEventListener('drop', function(e) { - - notifyDragEnd(e); - - const files = e.dataTransfer.files; - - // Ignore any non-files that are dragged into the drop area - if (files.length < 1) - return; - - if (files.length > 2) { - - // If more than one file was provided, print an error explaining - // that only a single file is allowed and abort processing - setError('IMPORT.ERROR_FILE_SINGLE_ONLY'); - return; - } - - handleFile(files[0]); - - }, false); - - /** - * The hidden file input used to create a file browser. - * - * @type Element - */ - const fileUploadInput = $element.find('.file-upload-input')[0]; - - /** - * A function that will click on the hidden file input to open a file - * browser to allow the user to select a file for upload. - */ - $scope.openFileBrowser = () => - $timeout(() => fileUploadInput.click(), 0, false); - - /** - * A handler that will be invoked when a user selectes a file in the - * file browser. After some error checking, the file will be passed to - * the onFile callback provided to this directive. - * - * @param {Event} e - * The event that was triggered when the user selected a file in - * their file browser. - */ - fileUploadInput.onchange = e => { - - // Process the uploaded file - handleFile(e.target.files[0]); - - // Clear the value to ensure that the change event will be fired - // if the user selects the same file again - fileUploadInput.value = null; - - }; - - }]; - return directive; - -}]); \ No newline at end of file diff --git a/guacamole/src/main/frontend/src/app/import/styles/file-upload.css b/guacamole/src/main/frontend/src/app/import/styles/file-upload.css deleted file mode 100644 index afe0fcf97..000000000 --- a/guacamole/src/main/frontend/src/app/import/styles/file-upload.css +++ /dev/null @@ -1,119 +0,0 @@ -/* - * 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. - */ - -.file-upload-container { - - display: flex; - flex-direction: column; - align-items: center; - padding: 24px 24px 24px; - - width: fit-content; - - border: 1px solid rgba(0,0,0,.25); - box-shadow: 1px 1px 2px rgb(0 0 0 / 25%); - - margin-left: auto; - margin-right: auto; - -} - -.file-upload-container .upload-header { - - display: flex; - flex-direction: row; - width: 500px; - margin-bottom: 5px; - justify-content: space-between; - -} - -.file-upload-container .file-error { - - color: red; - -} - -.file-upload-container .file-options { - - font-weight: bold; - -} - -.file-upload-container .file-upload-input { - - display: none; - -} - -.file-upload-container .drop-target { - - display: flex; - flex-direction: column; - - align-items: center; - justify-content: space-evenly; - - width: 500px; - height: 200px; - - background: rgba(0,0,0,.04); - border: 1px solid black; - -} - -.file-upload-container .drop-target.file-present { - - background: rgba(0,0,0,.15); - -} - - -.file-upload-container .drop-target .file-name { - - font-weight: bold; - font-size: 1.5em; - -} - -.file-upload-container .drop-target.drop-pending { - - background: #3161a9; - -} - -.file-upload-container .drop-target.drop-pending > * { - - opacity: 0.5; - -} - -.file-upload-container .drop-target .title { - - font-weight: bold; - font-size: 1.25em; - -} - -.file-upload-container .drop-target .browse-link { - - text-decoration: underline; - cursor: pointer; - -} 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 8f038e4d1..3fbdbe077 100644 --- a/guacamole/src/main/frontend/src/app/import/styles/import.css +++ b/guacamole/src/main/frontend/src/app/import/styles/import.css @@ -42,3 +42,104 @@ .import .errors .error-message ul { margin: 0px; } + +.file-upload-container { + + display: flex; + flex-direction: column; + align-items: center; + padding: 24px 24px 24px; + + width: fit-content; + + border: 1px solid rgba(0,0,0,.25); + box-shadow: 1px 1px 2px rgb(0 0 0 / 25%); + + margin-left: auto; + margin-right: auto; + +} + +.file-upload-container .upload-header { + + display: flex; + flex-direction: row; + width: 500px; + margin-bottom: 5px; + justify-content: space-between; + +} + +.file-upload-container .file-error { + + color: red; + +} + +.file-upload-container .file-options { + + font-weight: bold; + +} + +.file-upload-container .file-upload-input { + + display: none; + +} + +.file-upload-container .drop-target { + + display: flex; + flex-direction: column; + + align-items: center; + justify-content: space-evenly; + + width: 500px; + height: 200px; + + background: rgba(0,0,0,.04); + border: 1px solid black; + +} + +.file-upload-container .drop-target.file-present { + + background: rgba(0,0,0,.15); + +} + + +.file-upload-container .drop-target .file-name { + + font-weight: bold; + font-size: 1.5em; + +} + +.file-upload-container .drop-target.drop-pending { + + background: #3161a9; + +} + +.file-upload-container .drop-target.drop-pending > * { + + opacity: 0.5; + +} + +.file-upload-container .drop-target .title { + + font-weight: bold; + font-size: 1.25em; + +} + +.file-upload-container .drop-target .browse-link { + + text-decoration: underline; + cursor: pointer; + +} \ No newline at end of file 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 d0f9fd537..c9e37be46 100644 --- a/guacamole/src/main/frontend/src/app/import/templates/connectionImport.html +++ b/guacamole/src/main/frontend/src/app/import/templates/connectionImport.html @@ -4,8 +4,32 @@

{{'IMPORT.HEADER' | translate}}

+ +
+ +
+ {{'IMPORT.UPLOAD_FILE_TYPES' | translate}} + {{'IMPORT.UPLOAD_HELP_LINK' | translate}} + +
+ +
+ +
{{'IMPORT.UPLOAD_DROP_TITLE' | translate}}
+ + + + {{'IMPORT.UPLOAD_BROWSE_LINK' | translate}} + + +
{{fileName}}
+ +
+ +
-