diff --git a/docs/en_US/images/delete.png b/docs/en_US/images/delete.png deleted file mode 100644 index e5936e827..000000000 Binary files a/docs/en_US/images/delete.png and /dev/null differ diff --git a/docs/en_US/images/download.png b/docs/en_US/images/download.png deleted file mode 100644 index b3efacc74..000000000 Binary files a/docs/en_US/images/download.png and /dev/null differ diff --git a/docs/en_US/images/edit.png b/docs/en_US/images/edit.png deleted file mode 100644 index a52560bbf..000000000 Binary files a/docs/en_US/images/edit.png and /dev/null differ diff --git a/docs/en_US/images/folder.png b/docs/en_US/images/folder.png deleted file mode 100644 index 473632c46..000000000 Binary files a/docs/en_US/images/folder.png and /dev/null differ diff --git a/docs/en_US/images/gridview.png b/docs/en_US/images/gridview.png deleted file mode 100644 index 41edbac36..000000000 Binary files a/docs/en_US/images/gridview.png and /dev/null differ diff --git a/docs/en_US/images/home.png b/docs/en_US/images/home.png deleted file mode 100644 index 6c671548c..000000000 Binary files a/docs/en_US/images/home.png and /dev/null differ diff --git a/docs/en_US/images/refresh.png b/docs/en_US/images/refresh.png deleted file mode 100644 index 58738a14d..000000000 Binary files a/docs/en_US/images/refresh.png and /dev/null differ diff --git a/docs/en_US/images/sm_download.png b/docs/en_US/images/sm_download.png new file mode 100644 index 000000000..50471b760 Binary files /dev/null and b/docs/en_US/images/sm_download.png differ diff --git a/docs/en_US/images/sm_go_back.png b/docs/en_US/images/sm_go_back.png new file mode 100644 index 000000000..2fb4641ba Binary files /dev/null and b/docs/en_US/images/sm_go_back.png differ diff --git a/docs/en_US/images/sm_home.png b/docs/en_US/images/sm_home.png new file mode 100644 index 000000000..f6b13b6f8 Binary files /dev/null and b/docs/en_US/images/sm_home.png differ diff --git a/docs/en_US/images/sm_new_folder.png b/docs/en_US/images/sm_new_folder.png new file mode 100644 index 000000000..3e04e410f Binary files /dev/null and b/docs/en_US/images/sm_new_folder.png differ diff --git a/docs/en_US/images/sm_options.png b/docs/en_US/images/sm_options.png new file mode 100644 index 000000000..f12951fc7 Binary files /dev/null and b/docs/en_US/images/sm_options.png differ diff --git a/docs/en_US/images/sm_refresh.png b/docs/en_US/images/sm_refresh.png new file mode 100644 index 000000000..c7914c019 Binary files /dev/null and b/docs/en_US/images/sm_refresh.png differ diff --git a/docs/en_US/images/storage_manager.png b/docs/en_US/images/storage_manager.png index 4ad42ce91..ac33ea9fd 100644 Binary files a/docs/en_US/images/storage_manager.png and b/docs/en_US/images/storage_manager.png differ diff --git a/docs/en_US/images/tableview.png b/docs/en_US/images/tableview.png deleted file mode 100644 index 0191c4fe6..000000000 Binary files a/docs/en_US/images/tableview.png and /dev/null differ diff --git a/docs/en_US/images/uparrow.png b/docs/en_US/images/uparrow.png deleted file mode 100644 index 3c4303d85..000000000 Binary files a/docs/en_US/images/uparrow.png and /dev/null differ diff --git a/docs/en_US/images/upload.png b/docs/en_US/images/upload.png deleted file mode 100644 index ec9d0eb80..000000000 Binary files a/docs/en_US/images/upload.png and /dev/null differ diff --git a/docs/en_US/release_notes_6_12.rst b/docs/en_US/release_notes_6_12.rst index b2fe1d025..320ed9395 100644 --- a/docs/en_US/release_notes_6_12.rst +++ b/docs/en_US/release_notes_6_12.rst @@ -10,12 +10,15 @@ New features ************ | `Issue #4488 `_ - Added option to trigger autocomplete on key press in the query tool. + | `Issue #4607 `_ - Allow users to delete files/folders from the storage manager. + | `Issue #7389 `_ - Allow users to search within the file/storage manager. | `Issue #7486 `_ - Added support for visualizing the graphs using Stacked Line, Bar, and Stacked Bar charts in the query tool. | `Issue #7487 `_ - Added support for visualise the graph using a Pie chart in the query tool. Housekeeping ************ + | `Issue #7313 `_ - Port the file/storage manager to React. | `Issue #7341 `_ - Port change password dialog to React. | `Issue #7342 `_ - Port Master Password dialog to React. | `Issue #7492 `_ - Removing dynamic module loading and replacing it with static loading. @@ -34,3 +37,4 @@ Bug fixes | `Issue #7520 `_ - Fixed the JSON editor issue of hiding the first record. | `Issue #7522 `_ - Added support for Azure PostgreSQL deployment in server mode. | `Issue #7523 `_ - Fixed typo error for Statistics on the table header. + | `Issue #7524 `_ - Fixed an issue where new folders cannot be created in the save dialog. diff --git a/docs/en_US/storage_manager.rst b/docs/en_US/storage_manager.rst index 36bb5d6f8..aee923598 100644 --- a/docs/en_US/storage_manager.rst +++ b/docs/en_US/storage_manager.rst @@ -20,48 +20,54 @@ Use icons on the top of the *Storage Manager* window to manage storage: Use the ``Home`` icon |home| to return to the home directory. -.. |home| image:: images/home.png +.. |home| image:: images/sm_home.png Use the ``Up Arrow`` icon |uparrow| to return to the previous directory. -.. |uparrow| image:: images/uparrow.png +.. |uparrow| image:: images/sm_go_back.png Use the ``Refresh`` icon |refresh| to display the most-recent files available. -.. |refresh| image:: images/refresh.png +.. |refresh| image:: images/sm_refresh.png Select the ``Download`` icon |download| to download the selected file. -.. |download| image:: images/download.png - -Select the ``Delete`` icon |delete| to delete the selected file or folder. - -.. |delete| image:: images/delete.png - -Select the ``Edit`` icon |edit| to rename a file or folder. - -.. |edit| image:: images/edit.png - -Use the ``Upload`` icon |upload| to upload a file. - -.. |upload| image:: images/upload.png +.. |download| image:: images/sm_download.png Use the ``New Folder`` icon |folder| to add a new folder. -.. |folder| image:: images/folder.png - -Use the ``Grid View`` icon |gridview| to display all the files and folders in a grid view. - -.. |gridview| image:: images/gridview.png - -Use the ``Table View`` icon |tableview| to display all the files and folders in a list view. - -.. |tableview| image:: images/tableview.png - -Click on the check box next to *Show hidden files and folders* at the bottom of the window to view hidden files and folders. +.. |folder| image:: images/sm_new_folder.png Use the *Format* drop down list to select the format of the files to be displayed; choose from *sql*, *csv*, or *All Files*. +Other Options +********************* + +.. image:: images/sm_options.png + :alt: Other options + :align: center + +.. table:: + :class: longtable + :widths: 1 5 + + +----------------------+---------------------------------------------------------------------------------------------------+ + | Menu | Behavior | + +======================+===================================================================================================+ + | *Rename* | Click the *Rename* option to rename a file/folder. | + +----------------------+---------------------------------------------------------------------------------------------------+ + | *Delete* | Click the *Delete* option to rename a file/folder. | + +----------------------+---------------------------------------------------------------------------------------------------+ + | *Upload* | Click the *Upload* option to upload multiple files to the current folder. | + +----------------------+---------------------------------------------------------------------------------------------------+ + | *List View* | Click the *List View* option to to display all the files and folders in a list view. | + +----------------------+---------------------------------------------------------------------------------------------------+ + | *Grid View* | Click the *Grid View* option to to display all the files and folders in a grid view. | + +----------------------+---------------------------------------------------------------------------------------------------+ + | *Show Hidden Files* | Click the *Show Hidden Files* option to view hidden files and folders. | + +----------------------+---------------------------------------------------------------------------------------------------+ + + You can also download backup files through *Storage Manager* at the successful completion of the backups taken through :ref:`Backup Dialog `, :ref:`Backup Global Dialog `, or :ref:`Backup Server Dialog `. At the successful completion of a backup, click on the icon to open the current backup file in *Storage Manager* on the *process watcher* window. diff --git a/runtime/package.json b/runtime/package.json index 1051ba603..0015fbc95 100644 --- a/runtime/package.json +++ b/runtime/package.json @@ -7,6 +7,7 @@ "license": "PostgreSQL", "chromium-args": "--disable-popup-blocking --disable-gpu", "user-agent": "Nwjs:%nwver-%osinfo-%chromium_ver", + "nodejs": true, "window": { "width": 750, "height": 600, diff --git a/web/package.json b/web/package.json index 7e4b2cc36..c99217714 100644 --- a/web/package.json +++ b/web/package.json @@ -111,12 +111,12 @@ "closest": "^0.0.1", "codemirror": "^5.59.2", "context-menu": "^2.0.0", + "convert-units": "^2.3.4", "css-loader": "^5.0.1", "cssnano": "^5.0.2", "dagre": "^0.8.4", "date-fns": "^2.24.0", "diff-arrays-of-objects": "^1.1.8", - "dropzone": "^5.9.3", "html2canvas": "^1.0.0-rc.7", "immutability-helper": "^3.0.0", "imports-loader": "^2.0.0", @@ -150,6 +150,7 @@ "react-data-grid": "git+https://github.com/adityatoshniwal/react-data-grid.git/#8d9bc16ddd7c419acfbbd1c1cc2b70eb9f5b453c", "react-dom": "^17.0.1", "react-draggable": "^4.4.4", + "react-dropzone": "^14.2.1", "react-leaflet": "^3.2.2", "react-rnd": "^10.3.5", "react-router-dom": "^6.2.2", @@ -165,7 +166,6 @@ "socket.io-client": "^4.0.0", "split.js": "^1.5.10", "styled-components": "^5.2.1", - "tablesorter": "^2.31.2", "tempusdominus-bootstrap-4": "^5.1.2", "tempusdominus-core": "^5.19.3", "underscore": "^1.13.1", diff --git a/web/pgadmin/browser/static/js/node.js b/web/pgadmin/browser/static/js/node.js index 2d12c4ed9..199ee986f 100644 --- a/web/pgadmin/browser/static/js/node.js +++ b/web/pgadmin/browser/static/js/node.js @@ -551,7 +551,25 @@ define('pgadmin.browser.node', [ } }, + registerDockerPanel: function(docker, name, params) { + var w = docker || pgBrowser.docker, + p = w.findPanels(name); + if (p && p.length == 1) + return; + + p = new pgBrowser.Panel({ + name: name, + showTitle: true, + isCloseable: true, + isPrivate: true, + isLayoutMember: false, + canMaximise: true, + content: '
', + ...params, + }); + p.load(w); + }, registerUtilityPanel: function(docker) { var w = docker || pgBrowser.docker, p = w.findPanels('utility_props'); diff --git a/web/pgadmin/misc/bgprocess/static/js/bgprocess.js b/web/pgadmin/misc/bgprocess/static/js/bgprocess.js index b007fec3f..ddc965e7a 100644 --- a/web/pgadmin/misc/bgprocess/static/js/bgprocess.js +++ b/web/pgadmin/misc/bgprocess/static/js/bgprocess.js @@ -9,7 +9,7 @@ define('misc.bgprocess', [ 'sources/pgadmin', 'sources/gettext', 'sources/url_for', 'underscore', - 'jquery', 'pgadmin.browser', 'alertify', + 'jquery', 'pgadmin.browser', 'alertify', 'pgadmin.tools.file_manager', ], function( pgAdmin, gettext, url_for, _, $, pgBrowser, Alertify ) { @@ -625,9 +625,7 @@ define('misc.bgprocess', [ var self = this; if(self.current_storage_dir) { - pgBrowser.Events.trigger( - 'pgadmin:tools:storage_manager', self.current_storage_dir - ); + pgAdmin.Tools.FileManager.openStorageManager(self.current_storage_dir); } }, }); diff --git a/web/pgadmin/misc/file_manager/__init__.py b/web/pgadmin/misc/file_manager/__init__.py index f9562b1a8..bef74fb71 100644 --- a/web/pgadmin/misc/file_manager/__init__.py +++ b/web/pgadmin/misc/file_manager/__init__.py @@ -28,9 +28,11 @@ from flask_babel import gettext from flask_security import login_required from pgadmin.utils import PgAdminModule from pgadmin.utils import get_storage_directory -from pgadmin.utils.ajax import make_json_response +from pgadmin.utils.ajax import make_json_response, unauthorized, \ + internal_server_error from pgadmin.utils.preferences import Preferences from pgadmin.utils.constants import PREF_LABEL_OPTIONS, MIMETYPE_APP_JS +from pgadmin.settings.utils import get_file_type_setting # Checks if platform is Windows if _platform == "win32": @@ -132,9 +134,9 @@ class FileManagerModule(PgAdminModule): list: a list of url endpoints exposed to the client. """ return [ + 'file_manager.init', 'file_manager.filemanager', 'file_manager.index', - 'file_manager.get_trans_id', 'file_manager.delete_trans_id', 'file_manager.save_last_dir', 'file_manager.save_file_dialog_view', @@ -196,74 +198,62 @@ def utility(): mimetype=MIMETYPE_APP_JS) -@blueprint.route("/file_manager.js") -@login_required -def file_manager_js(): - """render the required javascript""" - return Response(response=render_template( - "file_manager/js/file_manager.js", _=gettext), - status=200, - mimetype=MIMETYPE_APP_JS) - - -@blueprint.route("/en.json") -@login_required -def language(): - """render the required javascript""" - return Response(response=render_template( - "file_manager/js/languages/en.json", _=gettext), - status=200) - - -@blueprint.route("/file_manager_config.js") -@login_required -def file_manager_config_js(): - """render the required javascript""" - return Response(response=render_template( - "file_manager/js/file_manager_config.js", _=gettext), - status=200, - mimetype=MIMETYPE_APP_JS) - - -@blueprint.route("//file_manager_config.json") -@login_required -def file_manager_config(trans_id): - """render the required json""" - data = Filemanager.get_trasaction_selection(trans_id) - pref = Preferences.module('file_manager') - file_dialog_view = pref.preference('file_dialog_view').get()[0] - show_hidden_files = pref.preference('show_hidden_files').get() - - return Response(response=render_template( - "file_manager/js/file_manager_config.json", - _=gettext, - data=data, - file_dialog_view=file_dialog_view, - show_hidden_files=show_hidden_files - ), - status=200, - mimetype="application/json" - ) - - @blueprint.route( - "/get_trans_id", methods=["GET", "POST"], endpoint='get_trans_id' + "/init", methods=["POST"], endpoint='init' ) @login_required -def get_trans_id(): +def init_filemanager(): if len(req.data) != 0: configs = json.loads(req.data) trans_id = Filemanager.create_new_transaction(configs) - global transid - transid = trans_id - return make_json_response( - data={'fileTransId': transid, 'status': True} - ) + data = Filemanager.get_trasaction_selection(trans_id) + pref = Preferences.module('file_manager') + file_dialog_view = pref.preference('file_dialog_view').get() + if type(file_dialog_view) == list: + file_dialog_view = file_dialog_view[0] + + last_selected_format = get_file_type_setting(data['supported_types']) + # in some cases, the setting may not match with available types + if last_selected_format not in data['supported_types']: + last_selected_format = data['supported_types'][0] + + res_data = { + 'transId': trans_id, + "options": { + "culture": "en", + "lang": "py", + "defaultViewMode": file_dialog_view, + "autoload": True, + "showFullPath": False, + "dialog_type": data['dialog_type'], + "show_hidden_files": + pref.preference('show_hidden_files').get(), + "fileRoot": data['fileroot'], + "capabilities": data['capabilities'], + "allowed_file_types": data['supported_types'], + "platform_type": data['platform_type'], + "show_volumes": data['show_volumes'], + "homedir": data['homedir'], + "last_selected_format": last_selected_format + }, + "security": { + "uploadPolicy": data['security']['uploadPolicy'], + "uploadRestrictions": data['security']['uploadRestrictions'], + }, + "upload": { + "multiple": data['upload']['multiple'], + "number": 20, + "fileSizeLimit": data['upload']['fileSizeLimit'], + "imagesOnly": False + } + } + + return make_json_response(data=res_data) @blueprint.route( - "/del_trans_id/", - methods=["GET", "POST"], endpoint='delete_trans_id' + "/delete_trans_id/", + methods=["DELETE"], endpoint='delete_trans_id' ) @login_required def delete_trans_id(trans_id): @@ -279,9 +269,7 @@ def delete_trans_id(trans_id): @login_required def save_last_directory_visited(trans_id): blueprint.last_directory_visited.set(req.json['path']) - return make_json_response( - data={'status': True} - ) + return make_json_response(status=200) @blueprint.route( @@ -291,9 +279,7 @@ def save_last_directory_visited(trans_id): @login_required def save_file_dialog_view(trans_id): blueprint.file_dialog_view.set(req.json['view']) - return make_json_response( - data={'status': True} - ) + return make_json_response(status=200) @blueprint.route( @@ -303,9 +289,7 @@ def save_file_dialog_view(trans_id): @login_required def save_show_hidden_file_option(trans_id): blueprint.show_hidden_files.set(req.json['show_hidden']) - return make_json_response( - data={'status': True} - ) + return make_json_response(status=200) class Filemanager(object): @@ -321,14 +305,6 @@ class Filemanager(object): def __init__(self, trans_id): self.trans_id = trans_id - self.patherror = encode_json( - { - 'Error': gettext( - 'No permission to operate on specified path.' - ), - 'Code': 0 - } - ) self.dir = get_storage_directory() if self.dir is not None and isinstance(self.dir, list): @@ -387,7 +363,7 @@ class Filemanager(object): # tuples with (capabilities, files_only, folders_only, title) capability_map = { 'select_file': ( - ['select_file', 'rename', 'upload', 'create'], + ['select_file', 'rename', 'upload', 'delete'], True, False, gettext("Select File") @@ -421,6 +397,8 @@ class Filemanager(object): # get last visited directory, if not present then traverse in reverse # order to find closest parent directory + if 'init_path' in params: + blueprint.last_directory_visited.get(params['init_path']) last_dir = blueprint.last_directory_visited.get() check_dir_exists = False if last_dir is None: @@ -436,9 +414,8 @@ class Filemanager(object): # create configs using above configs configs = { - # for JS json compatibility - "fileroot": last_dir.replace('\\', '\\\\'), - "homedir": homedir.replace('\\', '\\\\'), + "fileroot": last_dir, + "homedir": homedir, "dialog_type": fm_type, "title": title, "upload": { @@ -499,14 +476,14 @@ class Filemanager(object): file_manager_data = session['fileManagerData'] # Return from the function if transaction id not found if str(trans_id) not in file_manager_data: - return make_json_response(data={'status': True}) + return make_json_response(status=200) # Remove the information of unique transaction id # from the session variable. file_manager_data.pop(str(trans_id), None) session['fileManagerData'] = file_manager_data - return make_json_response(data={'status': True}) + return make_json_response(status=200) @staticmethod def _get_drives_with_size(drive_name=None): @@ -590,7 +567,7 @@ class Filemanager(object): :param orig_path: path after user dir :return: """ - files = {} + files = [] for f in sorted(os.listdir(orig_path)): system_path = os.path.join(os.path.join(orig_path, f)) @@ -617,14 +594,13 @@ class Filemanager(object): if files_only == 'true': continue file_extension = "dir" - user_path = "{0}/".format(user_path) # filter files based on file_type elif Filemanager._skip_file_extension( file_type, supported_types, folders_only, file_extension): continue # create a list of files and folders - files[f] = { + files.append({ "Filename": f, "Path": user_path, "file_type": file_extension, @@ -634,7 +610,7 @@ class Filemanager(object): "Date Modified": modified, "Size": sizeof_fmt(getsize(system_path)) } - } + }) return files @@ -649,23 +625,16 @@ class Filemanager(object): path = unquote(path) - try: - Filemanager.check_access_permission(in_dir, path) - except Exception as e: - Filemanager.resume_windows_warning() - files = { - 'Code': 0, - 'Error': str(e) - } - return files + Filemanager.check_access_permission(in_dir, path) + Filemanager.resume_windows_warning() - files = {} + files = [] if (_platform == "win32" and (path == '/' or path == '\\'))\ and in_dir is None: drives = Filemanager._get_drives_with_size() for drive, drive_size in drives: path = file_name = "{0}:".format(drive) - files[file_name] = { + files.append({ "Filename": file_name, "Path": path, "file_type": 'drive', @@ -675,7 +644,7 @@ class Filemanager(object): "Date Modified": "", "Size": drive_size } - } + }) Filemanager.resume_windows_warning() return files @@ -683,10 +652,9 @@ class Filemanager(object): if not path_exists(orig_path): Filemanager.resume_windows_warning() - return { - 'Code': 0, - 'Error': gettext("'{0}' file does not exist.").format(path) - } + return make_json_response( + status=404, + errormsg=gettext("'{0}' file does not exist.").format(path)) user_dir = path folders_only = trans_data.get('folders_only', '') @@ -705,11 +673,7 @@ class Filemanager(object): if (hasattr(e, 'strerror') and e.strerror == gettext('Permission denied')): err_msg = str(e.strerror) - - files = { - 'Code': 0, - 'Error': err_msg - } + return unauthorized(err_msg) Filemanager.resume_windows_warning() return files @@ -735,9 +699,10 @@ class Filemanager(object): # Do not allow user to access outside his storage dir # in server mode. - if not orig_path.startswith(in_dir): - raise InternalServerError( - gettext("Access denied ({0})").format(path)) + try: + pathlib.Path(orig_path).relative_to(in_dir) + except ValueError: + raise PermissionError(gettext("Access denied ({0})").format(path)) @staticmethod def get_abs_path(in_dir, path): @@ -789,33 +754,13 @@ class Filemanager(object): self.dir = "" orig_path = "{0}{1}".format(self.dir, path) - try: - Filemanager.check_access_permission(self.dir, path) - except Exception as e: - thefile = { - 'Filename': split_path(path)[-1], - 'FileType': '', - 'Path': path, - 'Error': str(e), - 'Code': 0, - 'Info': '', - 'Properties': { - date_created: '', - date_modified: '', - 'Width': '', - 'Height': '', - 'Size': '' - } - } - return thefile + Filemanager.check_access_permission(self.dir, path) user_dir = path thefile = { 'Filename': split_path(orig_path)[-1], 'FileType': '', 'Path': user_dir, - 'Error': '', - 'Code': 1, 'Info': '', 'Properties': { date_created: '', @@ -827,10 +772,9 @@ class Filemanager(object): } if not path_exists(orig_path): - thefile['Error'] = gettext( - "'{0}' file does not exist.").format(path) - thefile['Code'] = -1 - return thefile + return make_json_response( + status=404, + errormsg=gettext("'{0}' file does not exist.").format(path)) if split_path(user_dir)[-1] == '/'\ or os.path.isfile(orig_path) is False: @@ -868,19 +812,12 @@ class Filemanager(object): Rename file or folder """ if not self.validate_request('rename'): - return self.ERROR_NOT_ALLOWED + return unauthorized(self.ERROR_NOT_ALLOWED['Error']) the_dir = self.dir if self.dir is not None else '' - try: - Filemanager.check_access_permission(the_dir, old) - Filemanager.check_access_permission(the_dir, new) - except Exception as e: - res = { - 'Error': str(e), - 'Code': 0 - } - return res + Filemanager.check_access_permission(the_dir, old) + Filemanager.check_access_permission(the_dir, new) # check if it's dir if old[-1] == '/': @@ -906,39 +843,27 @@ class Filemanager(object): try: os.rename(oldpath_sys, newpath_sys) except Exception as e: - code = 0 - error_msg = "{0} {1}".format( - gettext('There was an error renaming the file:'), e) + return internal_server_error("{0} {1}".format( + gettext('There was an error renaming the file:'), e)) - result = { + return { 'Old Path': old, 'Old Name': oldname, 'New Path': newpath, 'New Name': newname, - 'Error': error_msg, - 'Code': code } - return result - def delete(self, path=None, req=None): """ Delete file or folder """ if not self.validate_request('delete'): - return self.ERROR_NOT_ALLOWED + return unauthorized(self.ERROR_NOT_ALLOWED['Error']) the_dir = self.dir if self.dir is not None else '' orig_path = "{0}{1}".format(the_dir, path) - try: - Filemanager.check_access_permission(the_dir, path) - except Exception as e: - res = { - 'Error': str(e), - 'Code': 0 - } - return res + Filemanager.check_access_permission(the_dir, path) err_msg = '' code = 1 @@ -948,23 +873,17 @@ class Filemanager(object): else: os.remove(orig_path) except Exception as e: - code = 0 - err_msg = str(e.strerror) + return internal_server_error("{0} {1}".format( + gettext('There was an error deleting the file:'), e)) - result = { - 'Path': path, - 'Error': err_msg, - 'Code': code - } - - return result + return make_json_response(status=200) def add(self, req=None): """ File upload functionality """ if not self.validate_request('upload'): - return self.ERROR_NOT_ALLOWED + return unauthorized(self.ERROR_NOT_ALLOWED['Error']) the_dir = self.dir if self.dir is not None else '' err_msg = '' @@ -986,7 +905,7 @@ class Filemanager(object): ) ).relative_to(the_dir) except ValueError: - return self.ERROR_NOT_ALLOWED + return unauthorized(self.ERROR_NOT_ALLOWED['Error']) with open(new_name, 'wb') as f: while True: @@ -996,25 +915,15 @@ class Filemanager(object): break f.write(data) except Exception as e: - code = 0 - err_msg = str(e.strerror) if hasattr(e, 'strerror') else str(e) + return internal_server_error("{0} {1}".format( + gettext('There was an error adding the file:'), e)) - try: - Filemanager.check_access_permission(the_dir, path) - except Exception as e: - res = { - 'Error': str(e), - 'Code': 0 - } - return res + Filemanager.check_access_permission(the_dir, path) - result = { + return { 'Path': path, 'Name': new_name, - 'Error': err_msg, - 'Code': code } - return result def is_file_exist(self, path, name, req=None): """ @@ -1026,48 +935,40 @@ class Filemanager(object): name = unquote(name) path = unquote(path) - try: - orig_path = "{0}{1}".format(the_dir, path) - Filemanager.check_access_permission( - the_dir, "{}{}".format(path, name)) - new_name = "{0}{1}".format(orig_path, name) - if not os.path.exists(new_name): - code = 0 - except Exception as e: + orig_path = "{0}{1}".format(the_dir, path) + Filemanager.check_access_permission( + the_dir, "{}{}".format(path, name)) + + new_name = "{0}{1}".format(orig_path, name) + if not os.path.exists(new_name): code = 0 - if hasattr(e, 'strerror'): - err_msg = str(e.strerror) - else: - err_msg = str(e) - result = { + return { 'Path': path, 'Name': name, - 'Error': err_msg, - 'Code': code + 'Code': code, } - return result - @staticmethod - def get_new_name(in_dir, path, new_name, count=1): + def get_new_name(in_dir, path, name): """ Utility to provide new name for folder if file with same name already exists """ - last_char = new_name[-1] - t_new_path = "{}/{}{}_{}".format(in_dir, path, new_name, count) - if last_char == 'r' and not path_exists(t_new_path): - return t_new_path, new_name - else: - last_char = int(t_new_path[-1]) + 1 - new_path = "{}/{}{}_{}".format(in_dir, path, new_name, last_char) - if path_exists(new_path): - count += 1 - return Filemanager.get_new_name(in_dir, path, new_name, count) + new_name = name + count = 0 + while True: + file_path = "{}{}/".format(path, new_name) + create_path = file_path + if in_dir != "": + create_path = "{}/{}".format(in_dir, file_path) + + if not path_exists(create_path): + return create_path, file_path, new_name else: - return new_path, new_name + count += 1 + new_name = "{}_{}".format(name, count) @staticmethod def check_file_for_bom_and_binary(filename, enc="utf-8"): @@ -1125,17 +1026,15 @@ class Filemanager(object): append({os.path.basename(filename): enc}) except IOError as ex: - status = False # we don't want to expose real path of file # so only show error message. if ex.strerror == 'Permission denied': - err_msg = str(ex.strerror) + return unauthorized(str(ex.strerror)) else: - err_msg = str(ex) + return internal_server_error(str(ex)) except Exception as ex: - status = False - err_msg = str(ex) + return internal_server_error(str(ex)) # Remove root storage path from error message # when running in Server mode @@ -1146,52 +1045,30 @@ class Filemanager(object): return status, err_msg, is_binary, is_startswith_bom, enc - def addfolder(self, path, name): + def addfolder(self, path, name, req=None): """ Functionality to create new folder """ if not self.validate_request('create'): - return self.ERROR_NOT_ALLOWED + return unauthorized(self.ERROR_NOT_ALLOWED['Error']) - the_dir = self.dir if self.dir is not None else '' + user_dir = self.dir if self.dir is not None else '' + Filemanager.check_access_permission(user_dir, "{}{}".format( + path, name)) + + create_path, new_path, new_name = \ + self.get_new_name(user_dir, path, name) try: - Filemanager.check_access_permission(the_dir, "{}{}".format( - path, name)) + os.mkdir(create_path) except Exception as e: - res = { - 'Error': str(e), - 'Code': 0 - } - return res - - if the_dir != "": - new_path = "{}/{}{}/".format(the_dir, path, name) - else: - new_path = "{}{}/".format(path, name) - - err_msg = '' - code = 1 - new_name = name - if not path_exists(new_path): - try: - os.mkdir(new_path) - except Exception as e: - code = 0 - err_msg = str(e.strerror) - else: - new_path, new_name = self.get_new_name(the_dir, path, name) - try: - os.mkdir(new_path) - except Exception as e: - code = 0 - err_msg = str(e.strerror) + return internal_server_error(str(e)) result = { 'Parent': path, + 'Path': new_path, 'Name': new_name, - 'Error': err_msg, - 'Code': code + 'Date Modified': time.ctime(time.time()) } return result @@ -1201,20 +1078,14 @@ class Filemanager(object): Functionality to download file """ if not self.validate_request('download'): - return self.ERROR_NOT_ALLOWED + return unauthorized(self.ERROR_NOT_ALLOWED['Error']) the_dir = self.dir if self.dir is not None else '' orig_path = "{0}{1}".format(the_dir, path) - try: - Filemanager.check_access_permission( - the_dir, "{}{}".format(path, path) - ) - except Exception as e: - resp = Response(str(e)) - resp.headers['Content-Disposition'] = \ - 'attachment; filename=' + name - return resp + Filemanager.check_access_permission( + the_dir, "{}{}".format(path, path) + ) name = os.path.basename(path) if orig_path and len(orig_path) > 0: @@ -1232,12 +1103,7 @@ class Filemanager(object): def permission(self, path=None, req=None): the_dir = self.dir if self.dir is not None else '' res = {'Code': 1} - try: - Filemanager.check_access_permission(the_dir, path) - except Exception as e: - err_msg = str(e) - res['Code'] = 0 - res['Error'] = err_msg + Filemanager.check_access_permission(the_dir, path) return res @@ -1272,9 +1138,12 @@ def file_manager(trans_id): } mode = req.args['mode'] + func = getattr(my_fm, mode) try: - func = getattr(my_fm, mode) res = func(**kwargs) - return make_json_response(data={'result': res, 'status': True}) - except Exception: - return getattr(my_fm, mode)(**kwargs) + except PermissionError as e: + return unauthorized(str(e)) + + if type(res) == Response: + return res + return make_json_response(data={'result': res, 'status': True}) diff --git a/web/pgadmin/misc/file_manager/static/js/FileManagerModule.jsx b/web/pgadmin/misc/file_manager/static/js/FileManagerModule.jsx new file mode 100644 index 000000000..2c8bd8aa6 --- /dev/null +++ b/web/pgadmin/misc/file_manager/static/js/FileManagerModule.jsx @@ -0,0 +1,121 @@ + +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2022, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// +import gettext from 'sources/gettext'; +import Notifier from '../../../../static/js/helpers/Notifier'; +import React from 'react'; +import FileManager from './components/FileManager'; +import { getBrowser } from '../../../../static/js/utils'; + +export default class FileManagerModule { + static instance; + + static getInstance(...args) { + if(!FileManagerModule.instance) { + FileManagerModule.instance = new FileManagerModule(...args); + } + return FileManagerModule.instance; + } + + constructor(pgAdmin) { + this.pgAdmin = pgAdmin; + } + + init() { + if(this.initialized) + return; + this.initialized = true; + + if(this.pgAdmin.server_mode == 'True') { + // Define the nodes on which the menus to be appear + this.pgAdmin.Browser.add_menus([{ + name: 'storage_manager', + module: this, + applies: ['tools'], + callback: 'openStorageManager', + priority: 11, + label: gettext('Storage Manager...'), + enable: true, + }]); + } + } + + openStorageManager(path) { + this.show({ + dialog_type: 'storage_dialog', + supported_types: ['sql', 'csv', 'json', '*'], + dialog_title: gettext('Storage Manager'), + path: path, + }); + } + + showInternal(params, onOK, onCancel, modalObj) { + const modal = modalObj || Notifier; + let title = params.dialog_title; + if(!title) { + if(params.dialog_type == 'create_file') { + title = gettext('Save File'); + } else if(params.dialog_type == 'select_file') { + title = gettext('Select File'); + } else { + title = gettext('Storage Manager'); + } + } + modal.showModal(title, (closeModal)=>{ + return ( + + ); + }, { + isResizeable: true, + onClose: onCancel, + dialogWidth: 700, dialogHeight: 400 + }); + } + + showNative(params, onOK, onCancel) { + // https://docs.nwjs.io/en/latest/References/Changes%20to%20DOM/ + let fileEle = document.createElement('input'); + let accept = params.supported_types?.map((v)=>(v=='*' ? '' : `.${v}`))?.join(','); + fileEle.setAttribute('type', 'file'); + fileEle.setAttribute('accept', accept); + fileEle.onchange = (e)=>{ + if(e.target.value) { + onOK?.(e.target.value); + } else { + onCancel?.(); + } + }; + if(params.dialog_type == 'create_file') { + fileEle.setAttribute('nwsaveas', ''); + } else if(params.dialog_type == 'select_folder') { + fileEle.setAttribute('nwdirectory', ''); + } + fileEle.dispatchEvent(new MouseEvent('click')); + } + + show(params, onOK, onCancel, modalObj) { + let {name: browser} = getBrowser(); + if(browser == 'Nwjs') { + try { + this.showNative(params, onOK, onCancel); + } catch { + // Fall back to internal + this.showInternal(params, onOK, onCancel, modalObj); + } + } else { + // Fall back to internal + this.showInternal(params, onOK, onCancel, modalObj); + } + } +} diff --git a/web/pgadmin/misc/file_manager/static/js/components/FileManager.jsx b/web/pgadmin/misc/file_manager/static/js/components/FileManager.jsx new file mode 100644 index 000000000..2a75b2733 --- /dev/null +++ b/web/pgadmin/misc/file_manager/static/js/components/FileManager.jsx @@ -0,0 +1,770 @@ +import { Box, makeStyles } from '@material-ui/core'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { DefaultButton, PgButtonGroup, PgIconButton, PrimaryButton } from '../../../../../static/js/components/Buttons'; +import { useModalStyles } from '../../../../../static/js/helpers/ModalProvider'; +import CloseIcon from '@material-ui/icons/CloseRounded'; +import CheckRoundedIcon from '@material-ui/icons/CheckRounded'; +import HomeRoundedIcon from '@material-ui/icons/HomeRounded'; +import ArrowUpwardRoundedIcon from '@material-ui/icons/ArrowUpwardRounded'; +import MoreHorizRoundedIcon from '@material-ui/icons/MoreHorizRounded'; +import SyncRoundedIcon from '@material-ui/icons/SyncRounded'; +import CreateNewFolderRoundedIcon from '@material-ui/icons/CreateNewFolderRounded'; +import GetAppRoundedIcon from '@material-ui/icons/GetAppRounded'; +import gettext from 'sources/gettext'; +import clsx from 'clsx'; +import { FormFooterMessage, InputSelectNonSearch, InputText, MESSAGE_TYPE } from '../../../../../static/js/components/FormComponents'; +import ListView from './ListView'; +import { PgMenu, PgMenuDivider, PgMenuItem, usePgMenuGroup } from '../../../../../static/js/components/Menu'; +import getApiInstance, { parseApiError } from '../../../../../static/js/api_instance'; +import Loader from 'sources/components/Loader'; +import url_for from 'sources/url_for'; +import Uploader from './Uploader'; +import GridView from './GridView'; +import convert from 'convert-units'; +import PropTypes from 'prop-types'; +import { downloadBlob } from '../../../../../static/js/utils'; +import ErrorBoundary from '../../../../../static/js/helpers/ErrorBoundary'; + + +const useStyles = makeStyles((theme)=>({ + footerSaveAs: { + justifyContent: 'initial', + padding: '4px 8px', + display: 'flex', + alignItems: 'center', + }, + footer1: { + justifyContent: 'space-between', + padding: '4px 8px', + display: 'flex', + alignItems: 'center', + }, + toolbar: { + padding: '4px', + display: 'flex', + ...theme.mixins.panelBorder?.bottom, + }, + inputFilename: { + lineHeight: 1, + width: '100%', + }, + inputSearch: { + marginLeft: '4px', + lineHeight: 1, + width: '130px', + }, + formatSelect: { + '& .MuiSelect-select': { + paddingTop: '4px', + paddingBottom: '4px', + } + }, + replaceOverlay: { + position: 'absolute', + top: 0, + bottom: 0, + left: 0, + right: 0, + backgroundColor: theme.otherVars.loader.backgroundColor, + zIndex: 2, + display: 'flex', + }, + replaceDialog: { + margin: 'auto', + marginLeft: '1rem', + marginRight: '1rem', + color: theme.palette.text.primary, + backgroundColor: theme.palette.background.default, + width: '100%', + ...theme.mixins.panelBorder.all, + } +})); + +export function getComparator(sortColumn) { + const key = sortColumn?.columnKey; + const dir = sortColumn?.direction == 'ASC' ? 1 : -1; + switch (key) { + case 'Filename': + return (a, b) => { + return dir*(a['Filename'].localeCompare(b['Filename'])); + }; + case 'Properties.DateModified': + return (a, b) => { + try { + let a1 = new Date(a['Properties']['Date Modified']); + let b1 = new Date(b['Properties']['Date Modified']); + if(a1 > b1) return dir*1; + return dir*(a1 < b1 ? -1 : 0); + } catch { + return 0; + } + }; + case 'Properties.Size': + return (a, b) => { + const parseAndConvert = (columnVal)=>{ + if(columnVal.file_type != 'dir' && columnVal.file_type != 'drive' && columnVal['Properties']['Size']) { + let [size, unit] = columnVal['Properties']['Size'].split(' '); + return convert(size).from(unit.toUpperCase()).to('B'); + } + return -1; + }; + try { + let a1 = parseAndConvert(a); + let b1 = parseAndConvert(b); + if(a1 > b1) return dir*1; + return dir*(a1 < b1 ? -1 : 0); + } catch { + return 0; + } + }; + default: + return ()=>0; + } +} + +export class FileManagerUtils { + constructor(api, params) { + this.api = api; + this.params = params; + this.config = {}; + this.currPath = ''; + this.separator = '/'; + } + + get transId() { + return this.config.transId; + } + + get fileConnectorUrl() { + return `${url_for('file_manager.index')}filemanager/${this.transId}/`; + } + + get fileRoot() { + return this.config.options.fileRoot; + } + + get allowedFileTypes() { + return this.config.options?.allowed_file_types || []; + } + + get showHiddenFiles() { + return this.config.options?.show_hidden_files; + } + + set showHiddenFiles(val) { + this.config.options.show_hidden_files = val; + this.api.put(url_for('file_manager.save_show_hidden_file_option', { + trans_id: this.transId, + }), { + show_hidden: val, + }).catch((error)=>{ + console.error(error); + /* Do nothing */ + }); + } + + hasCapability(val) { + return this.config?.options?.capabilities?.includes(val); + } + + async initialize() { + let res = await this.api.post(url_for('file_manager.init'), this.params); + this.config = res.data.data; + if(this.config.options.platform_type == 'win32') { + this.separator = '\\'; + } + } + + join(path1, path2) { + if(path1.endsWith(this.separator)) { + return path1 + path2; + } + return path1 + this.separator + path2; + } + + getExt(filename) { + if (filename.split('.').length == 1) { + return ''; + } + return filename.split('.').pop(); + } + + async getFolder(path) { + const newPath = path || this.fileRoot; + let res = await this.api.post(this.fileConnectorUrl, { + 'path': newPath, + 'mode': 'getfolder', + 'file_type': this.config.options.last_selected_format || '*', + 'show_hidden': this.showHiddenFiles, + }); + this.currPath = newPath; + return res.data.data.result; + } + + async addFolder(row) { + let res = await this.api.post(this.fileConnectorUrl, { + 'path': this.currPath, + 'mode': 'addfolder', + 'name': row.Filename, + }); + return { + Filename: res.data.data.result.Name, + Path: res.data.data.result.Path, + file_type: 'dir', + Properties: { + 'Date Modified': res.data.data.result['Date Modified'], + } + }; + } + + async renameItem(row) { + let res = await this.api.post(this.fileConnectorUrl, { + 'mode': 'rename', + 'old': row.Path, + 'new': row.Filename, + }); + return { + ...row, + Path: res.data.data.result['New Path'], + Filename: res.data.data.result['New Name'], + }; + } + + async deleteItem(row, fileName) { + const path = fileName ? this.join(row.Path, fileName) : row.Path; + await this.api.post(this.fileConnectorUrl, { + 'mode': 'delete', + 'path': path, + }); + return path; + } + + async uploadItem(fileObj, onUploadProgress) { + const formData = new FormData(); + formData.append('newfile', fileObj); + formData.append('mode', 'add'); + formData.append('currentpath', this.join(this.currPath, '')); + return this.api({ + method: 'POST', + url: this.fileConnectorUrl, + headers: { 'Content-Type': 'multipart/form-data' }, + data: formData, + onUploadProgress: onUploadProgress, + maxContentLength: Infinity, + maxBodyLength: Infinity, + }); + } + + async setLastVisitedDir(path) { + return this.api.post(url_for('file_manager.save_last_dir', { + trans_id: this.transId, + }), { + 'path': path, + }); + } + + async downloadFile(row) { + let res = await this.api({ + method: 'POST', + url: this.fileConnectorUrl, + responseType: 'blob', + data: { + 'mode': 'download', + 'path': row.Path, + }, + }); + downloadBlob(res.data, res.headers.filename); + } + + setDialogView(view) { + this.config.options.defaultViewMode = view; + this.api.post(url_for('file_manager.save_file_dialog_view', { + trans_id: this.transId, + }), {view: view}) + .catch((err)=>{ + /* Do not fail anything */ + console.error(err); + }); + } + + setFileType(fileType) { + this.config.options.last_selected_format = fileType; + this.api.post(url_for('settings.save_file_format_setting'), this.config.options) + .catch((err)=>{ + /* Do not fail anything */ + console.error(err); + }); + } + + async checkPermission(path) { + let res = await this.api.post(this.fileConnectorUrl, { + 'path': path, + 'mode': 'permission', + }); + if (res.data.data.result.Code === 1) { + return null; + } else { + return res.data.data.result.Error; + } + } + + async isFileExists(path, fileName) { + let res = await this.api.post(this.fileConnectorUrl, { + 'path': path, + 'name': fileName, + 'mode': 'is_file_exist', + }); + return Boolean(res.data.data.result.Code); + } + + async destroy() { + await this.api.delete(url_for('file_manager.delete_trans_id', { + 'trans_id': this.transId, + })); + } + + isWinDrive(text) { + return text && text.length == 2 && text.endsWith(':') && this.config?.options?.platform_type == 'win32'; + } + + dirname(path) { + let ret = path; + if(!path) { + return ret; + } + if(path.endsWith(this.separator)) { + ret = ret.slice(0, -1); + } + if(this.isWinDrive(ret)) { + ret = this.separator; + } else { + ret = ret.slice(0, ret.lastIndexOf(this.separator)+1); + } + return ret; + } +} + +function ConfirmFile({text, onYes, onNo}) { + const classes = useStyles(); + const modalClasses = useModalStyles(); + return ( + + + {text}{} + + } onClick={onNo} >{gettext('No')} + } + onClick={onYes} autoFocus>{gettext('Yes')} + + + + ); +} +ConfirmFile.propTypes = { + text: PropTypes.string, + onYes: PropTypes.func, + onNo: PropTypes.func +}; + +export default function FileManager({params, closeModal, onOK, onCancel}) { + const classes = useStyles(); + const modalClasses = useModalStyles(); + const apiObj = useMemo(()=>getApiInstance(), []); + const fmUtilsObj = useMemo(()=>new FileManagerUtils(apiObj, params), []); + + const {openMenuName, toggleMenu, onMenuClose} = usePgMenuGroup(); + const [loaderText, setLoaderText] = useState('Loading...'); + const [items, setItems] = useState([]); + const [path, setPath] = useState(''); + const [errorMsg, setErrorMsg] = useState(''); + const [search, setSearch] = useState(''); + const [saveAs, setSaveAs] = useState(''); + const [okBtnDisable, setOkBtnDisable] = useState(true); + const [viewMode, setViewMode] = useState('list'); + const [showUploader, setShowUploader] = useState(false); + const [[confirmText, onConfirmYes], setConfirmFile] = useState([null, null]); + const [fileType, setFileType] = useState('*'); + const [sortColumns, setSortColumns] = useState([]); + const [selectedRow, setSelectedRow] = useState(); + const selectedRowIdx = useRef(); + const optionsRef = React.useRef(null); + const saveAsRef = React.useRef(null); + const [operation, setOperation] = useState({ + type: null, idx: null + }); + + const sortedItems = useMemo(()=>( + [...items].sort(getComparator(sortColumns[0])) + ), [items, sortColumns]); + + const filteredItems = useMemo(()=>{ + return sortedItems.filter((i)=>i.Filename?.toLowerCase().includes(search?.toLocaleLowerCase())); + }, [items, sortColumns, search]); + + const itemsText = useMemo(()=>{ + let suffix = items.length == 1 ? 'item' : 'items'; + if(items.length == filteredItems.length) { + return `${items.length} ${suffix}`; + } + return `${filteredItems.length} of ${items.length} ${suffix}`; + }, [items, filteredItems]); + + const openDir = async (dirPath)=>{ + setErrorMsg(''); + setLoaderText('Loading...'); + try { + if(fmUtilsObj.isWinDrive(dirPath)) { + dirPath += fmUtilsObj.separator; + } + let newItems = await fmUtilsObj.getFolder(dirPath || fmUtilsObj.currPath); + setItems(newItems); + setPath(fmUtilsObj.currPath); + params.dialog_type == 'storage_dialog' && fmUtilsObj.setLastVisitedDir(fmUtilsObj.currPath); + } catch (error) { + console.error(error); + setErrorMsg(parseApiError(error)); + } + setLoaderText(''); + }; + const completeOperation = async (oldRow, newRow, rowIdx, func)=>{ + setOperation({}); + if(oldRow?.Filename == newRow.Filename) { + setItems((prev)=>[ + ...prev.slice(0, rowIdx), + oldRow, + ...prev.slice(rowIdx+1) + ]); + return; + } + setItems((prev)=>[ + ...prev.slice(0, rowIdx), + newRow, + ...prev.slice(rowIdx+1) + ]); + try { + const actualRow = await func(newRow); + setItems((prev)=>[ + ...prev.slice(0, rowIdx), + actualRow, + ...prev.slice(rowIdx+1) + ]); + } catch (error) { + setErrorMsg(parseApiError(error)); + if(oldRow) { + setItems((prev)=>[ + ...prev.slice(0, rowIdx), + oldRow, + ...prev.slice(rowIdx+1) + ]); + } else { + setItems((prev)=>[ + ...prev.slice(0, rowIdx), + ...prev.slice(rowIdx+1) + ]); + } + } + }; + const onDownload = async ()=>{ + setLoaderText('Downloading...'); + try { + await fmUtilsObj.downloadFile(filteredItems[selectedRowIdx.current]); + } catch (error) { + setErrorMsg(parseApiError(error)); + console.error(error); + } + setLoaderText(''); + }; + const onAddFolder = ()=>{ + setItems((prev)=>[ + {Filename: 'Untitled Folder', file_type: 'dir'}, + ...prev, + ]); + setOperation({ + type: 'add', + idx: 0, + onComplete: async (row, rowIdx)=>{ + setErrorMsg(''); + setLoaderText('Creating folder...'); + await completeOperation(null, row, rowIdx, fmUtilsObj.addFolder.bind(fmUtilsObj)); + setLoaderText(''); + } + }); + }; + const renameSelectedItem = (e)=>{ + e.keepOpen = false; + setErrorMsg(''); + if(_.isUndefined(selectedRowIdx.current) || _.isNull(selectedRowIdx.current)) { + return; + } + setOperation({ + type: 'rename', + idx: selectedRowIdx.current, + onComplete: async (row, rowIdx)=>{ + setErrorMsg(''); + setLoaderText('Renaming...'); + let oldRow = items[rowIdx]; + await completeOperation(oldRow, row, rowIdx, fmUtilsObj.renameItem.bind(fmUtilsObj)); + setLoaderText(''); + } + }); + }; + const deleteSelectedItem = async (e)=>{ + e.keepOpen = false; + setErrorMsg(''); + if(_.isUndefined(selectedRowIdx.current) || _.isNull(selectedRowIdx.current)) { + return; + } + setConfirmFile([gettext('Are you sure you want to delete this file/folder?'), async ()=>{ + setConfirmFile([null, null]); + setLoaderText('Deleting...'); + try { + await fmUtilsObj.deleteItem(items[selectedRowIdx.current]); + setItems((prev)=>[ + ...prev.slice(0, selectedRowIdx.current), + ...prev.slice(selectedRowIdx.current+1), + ]); + } catch (error) { + setErrorMsg(parseApiError(error)); + console.error(error); + } + setLoaderText(''); + }]); + }; + const toggleViewMode = (e, val)=>{ + e.keepOpen = false; + setViewMode(val); + fmUtilsObj.setDialogView(val); + }; + const onOkClick = useCallback(async ()=>{ + setLoaderText('Please wait...'); + let onOkPath = null; + if(params.dialog_type == 'create_file') { + let newFileName = saveAs; + // Add the extension if user has not added. + if(fileType != '*' && !newFileName.endsWith(`.${fileType}`)) { + newFileName += `.${fileType}`; + } + onOkPath = fmUtilsObj.join(fmUtilsObj.currPath, newFileName); + let error = await fmUtilsObj.checkPermission(onOkPath); + if(error) { + setErrorMsg(error); + setLoaderText(''); + return; + } + let exists = await fmUtilsObj.isFileExists(fmUtilsObj.currPath, newFileName); + if(exists) { + setLoaderText(''); + setConfirmFile([gettext('Are you sure you want to replace this file?'), async ()=>{ + await fmUtilsObj.setLastVisitedDir(fmUtilsObj.currPath); + onOK?.(onOkPath); + closeModal(); + }]); + return; + } + } else if(selectedRowIdx?.current >= 0 && filteredItems[selectedRowIdx?.current]) { + onOkPath = filteredItems[selectedRowIdx?.current]['Path']; + } + await fmUtilsObj.setLastVisitedDir(fmUtilsObj.currPath); + onOK?.(onOkPath); + closeModal(); + }, [filteredItems, saveAs, fileType]); + const onItemEnter = useCallback(async (row)=>{ + if(row.file_type == 'dir' || row.file_type == 'drive') { + await openDir(row.Path); + } else { + if(params.dialog_type == 'select_file') { + onOkClick(); + } + } + }, [filteredItems]); + const onItemSelect = useCallback((idx)=>{ + selectedRowIdx.current = idx; + fewBtnDisableCheck(); + }, [filteredItems]); + const onItemClick = useCallback((idx)=>{ + let row = filteredItems[selectedRowIdx.current]; + if(params.dialog_type == 'create_file' && row?.file_type != 'dir' && row.file_type != 'drive') { + setSaveAs(filteredItems[idx]?.Filename); + } + }, [filteredItems]); + const fewBtnDisableCheck = ()=>{ + let disabled = true; + let row = filteredItems[selectedRowIdx.current]; + if(params.dialog_type == 'create_file') { + disabled = !saveAs.trim(); + } else if(selectedRowIdx.current >= 0 && row) { + let selectedfileType = row?.file_type; + if(((selectedfileType == 'dir' || selectedfileType == 'drive') && fmUtilsObj.hasCapability('select_folder')) + || (selectedfileType != 'dir' && selectedfileType != 'drive' && fmUtilsObj.hasCapability('select_file'))) { + disabled = false; + } + } + setOkBtnDisable(disabled); + setSelectedRow(row); + }; + useEffect(()=>{ + const init = async ()=>{ + await fmUtilsObj.initialize(); + if(params.dialog_type != 'select_folder') { + setFileType(fmUtilsObj.config?.options?.last_selected_format || '*'); + } + if(fmUtilsObj.config?.options?.defaultViewMode) { + setViewMode(fmUtilsObj.config?.options?.defaultViewMode); + } else { + setViewMode('list'); + } + openDir(params?.path); + params?.path && fmUtilsObj.setLastVisitedDir(params?.path); + }; + init(); + setTimeout(()=>{ + saveAsRef.current && saveAsRef.current.focus(); + }, 300); + return ()=>{ + fmUtilsObj.destroy(); + }; + }, []); + + useEffect(()=>{ + fewBtnDisableCheck(); + }, [saveAs, filteredItems.length]); + + const isNoneSelected = _.isUndefined(selectedRow); + let okBtnText = params.btn_primary; + if(!okBtnText) { + okBtnText = gettext('Select'); + if(params.dialog_type == 'create_file' || params.dialog_type == 'create_folder') { + okBtnText = gettext('Create'); + } + } + return ( + + + + + {Boolean(confirmText) && setConfirmFile([null, null])} onYes={onConfirmYes}/>} + + + { + await openDir(fmUtilsObj.config?.options?.homedir); + }} icon={} disabled={showUploader} /> + { + await openDir(fmUtilsObj.dirname(fmUtilsObj.currPath)); + }} icon={} disabled={!fmUtilsObj.dirname(fmUtilsObj.currPath) || showUploader} /> + { + if(e.code === 'Enter') { + e.preventDefault(); + await openDir(path); + } + }} value={path} onChange={setPath} readonly={showUploader} /> + { + await openDir(); + }} icon={} disabled={showUploader} /> + + + + {params.dialog_type == 'storage_dialog' && + } + onClick={onDownload} disabled={showUploader || isNoneSelected || selectedRow?.file_type == 'dir' || selectedRow?.file_type == 'drive'} />} + {fmUtilsObj.hasCapability('create') && } + onClick={onAddFolder} disabled={showUploader} />} + + + } + name="menu-options" ref={optionsRef} onClick={toggleMenu} disabled={showUploader} /> + + + {fmUtilsObj.hasCapability('rename') && + {gettext('Rename')} + } + {fmUtilsObj.hasCapability('delete') && + {gettext('Delete')} + } + {fmUtilsObj.hasCapability('upload') && <> + + { + e.keepOpen = false; + setShowUploader(true); + }}>{gettext('Upload')} + } + + toggleViewMode(e, 'list')}>{gettext('List View')} + toggleViewMode(e, 'grid')}>{gettext('Grid View')} + + { + e.keepOpen = false; + fmUtilsObj.showHiddenFiles = !fmUtilsObj.showHiddenFiles; + await openDir(); + }}>{gettext('Show Hidden Files')} + + + + {showUploader && + { + setShowUploader(false); + if(filesUploaded) { + await openDir(); + } + }}/>} + {viewMode == 'list' && + } + {viewMode == 'grid' && + } + setErrorMsg('')} /> + {params.dialog_type == 'create_file' && + + Save As + + } + {params.dialog_type != 'select_folder' && + + {itemsText} + + File Format + { + let val = e.target.value; + fmUtilsObj.setFileType(val); + openDir(fmUtilsObj.currPath); + setFileType(val); + }} + options={fmUtilsObj.allowedFileTypes?.map((type)=>({ + label: type == '*' ? gettext('All Files') : type, value: type + }))} /> + + } + + + + + + } onClick={()=>{ + onCancel?.(); + closeModal(); + }} >{gettext('Cancel')} + {params.dialog_type != 'storage_dialog' && + } + onClick={onOkClick} disabled={okBtnDisable || showUploader}>{okBtnText}} + + + + ); +} + +FileManager.propTypes = { + params: PropTypes.object, + closeModal: PropTypes.func, + onOK: PropTypes.func, + onCancel: PropTypes.func, +}; diff --git a/web/pgadmin/misc/file_manager/static/js/components/FileManagerConstants.js b/web/pgadmin/misc/file_manager/static/js/components/FileManagerConstants.js new file mode 100644 index 000000000..657bf66f5 --- /dev/null +++ b/web/pgadmin/misc/file_manager/static/js/components/FileManagerConstants.js @@ -0,0 +1,11 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2022, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// +export const FILE_MANGER_EVENTS = { + ADD_FOLDER: 'ADD_FOLDER' +}; diff --git a/web/pgadmin/misc/file_manager/static/js/components/GridView.jsx b/web/pgadmin/misc/file_manager/static/js/components/GridView.jsx new file mode 100644 index 000000000..39254aeae --- /dev/null +++ b/web/pgadmin/misc/file_manager/static/js/components/GridView.jsx @@ -0,0 +1,141 @@ +import { Box, makeStyles } from '@material-ui/core'; +import React, {useState, useEffect, useRef, useLayoutEffect} from 'react'; +import FolderIcon from '@material-ui/icons/Folder'; +import DescriptionIcon from '@material-ui/icons/Description'; +import LockRoundedIcon from '@material-ui/icons/LockRounded'; +import StorageRoundedIcon from '@material-ui/icons/StorageRounded'; +import PropTypes from 'prop-types'; + + +const useStyles = makeStyles((theme)=>({ + grid: { + display: 'flex', + fontSize: '13px', + flexWrap: 'wrap', + overflow: 'hidden', + }, + gridItem: { + width: '100px', + margin: '4px', + textAlign: 'center', + position: 'relative', + }, + gridItemContent: { + padding: '4px', + border: '1px solid transparent', + cursor: 'pointer', + '&[aria-selected=true]': { + backgroundColor: theme.palette.primary.light, + color: theme.otherVars.qtDatagridSelectFg, + borderColor: theme.palette.primary.main, + }, + }, + gridFilename: { + overflowWrap: 'break-word', + }, + gridItemEdit: { + border: `1px solid ${theme.otherVars.inputBorderColor}`, + backgroundColor: theme.palette.background.default, + }, + protected: { + height: '1.25rem', + width: '1.25rem', + position: 'absolute', + left: '52px', + color: theme.palette.error.main, + backgroundColor: 'inherit', + } +})); + +export function ItemView({idx, row, selected, onItemSelect, onItemEnter, onEditComplete}) { + const classes = useStyles(); + const editMode = Boolean(onEditComplete); + const fileNameRef = useRef(); + + useLayoutEffect(()=>{ + if(editMode) { + fileNameRef.current?.focus(); + } + }, [editMode]); + + const handleKeyDown = (e)=>{ + if(e.code == 'Tab') { + e.stopPropagation(); + } + if(e.code == 'Enter') { + onEditComplete({...row, Filename: fileNameRef.current.textContent?.trim()}); + } + if(e.code == 'Escape') { + e.preventDefault(); + e.stopPropagation(); + fileNameRef.current.textContent = row.Filename; + onEditComplete(row); + } + }; + + let icon = ; + if(row.file_type == 'dir') { + icon = ; + } else if(row.file_type == 'drive') { + icon = ; + } + + return ( +
  • +
    onItemSelect(idx)} onDoubleClick={()=>onItemEnter(row)}> +
    + {icon} + {Boolean(row.Protected) && } +
    +
    onEditComplete(row)} + className={editMode ? classes.gridItemEdit : classes.gridFilename} suppressContentEditableWarning={true} + contentEditable={editMode} data-test="filename-div">{row['Filename']}
    +
    +
  • + ); +} +ItemView.propTypes = { + idx: PropTypes.number, + row: PropTypes.object, + selected: PropTypes.bool, + onItemSelect: PropTypes.func, + onItemEnter: PropTypes.func, + onEditComplete: PropTypes.func, +}; + +export default function GridView({items, operation, onItemSelect, onItemEnter}) { + const classes = useStyles(); + const [selectedIdx, setSelectedIdx] = useState(null); + const gridRef = useRef(); + + useEffect(()=>{ + onItemSelect(selectedIdx); + }, [selectedIdx]); + + + let onEditComplete = null; + if(operation?.onComplete) { + onEditComplete = (row)=>{ + operation?.onComplete?.(row, operation.idx); + }; + } + + return ( + +
      + {items.map((item, i)=>( + ) + )} +
    + {items.length == 0 && No files/folders found} +
    + ); +} + +GridView.propTypes = { + items: PropTypes.arrayOf(PropTypes.object), + operation: PropTypes.object, + onItemSelect: PropTypes.func, + onItemEnter: PropTypes.func, +}; diff --git a/web/pgadmin/misc/file_manager/static/js/components/ListView.jsx b/web/pgadmin/misc/file_manager/static/js/components/ListView.jsx new file mode 100644 index 000000000..f8879be9a --- /dev/null +++ b/web/pgadmin/misc/file_manager/static/js/components/ListView.jsx @@ -0,0 +1,239 @@ +import { Box, makeStyles } from '@material-ui/core'; +import React, { useContext, useRef, useEffect } from 'react'; +import { Row } from 'react-data-grid'; +import PgReactDataGrid from '../../../../../static/js/components/PgReactDataGrid'; +import FolderIcon from '@material-ui/icons/Folder'; +import StorageRoundedIcon from '@material-ui/icons/StorageRounded'; +import DescriptionIcon from '@material-ui/icons/Description'; +import LockRoundedIcon from '@material-ui/icons/LockRounded'; +import KeyboardArrowUpIcon from '@material-ui/icons/KeyboardArrowUp'; +import KeyboardArrowDownIcon from '@material-ui/icons/KeyboardArrowDown'; +import PropTypes from 'prop-types'; + +const useStyles = makeStyles((theme)=>({ + grid: { + fontSize: '13px', + '& .rdg-header-row': { + '& .rdg-cell': { + padding: '0px 4px', + } + }, + '& .rdg-cell': { + padding: '0px 4px', + '&[aria-colindex="1"]': { + padding: '0px 4px', + '&.rdg-editor-container': { + padding: '0px', + }, + } + } + }, + input: { + appearance: 'none', + width: '100%', + height: '100%', + verticalAlign: 'top', + outline: 'none', + backgroundColor: theme.palette.background.default, + color: theme.palette.text.primary, + border: 0, + boxShadow: 'inset 0 0 0 1.5px '+theme.palette.primary.main, + padding: '0 2px', + '::selection': { + background: theme.palette.primary.light, + } + }, + protected: { + height: '0.75rem', + width: '0.75rem', + position: 'absolute', + left: '14px', + top: '5px', + color: theme.palette.error.main, + backgroundColor: 'inherit', + } +})); + +export const GridContextUtils = React.createContext(); + +export function FileNameEditor({row, column, onRowChange, onClose}) { + const classes = useStyles(); + const value = row[column.key] ?? ''; + const [localVal, setLocalVal] = React.useState(value); + const localValRef = useRef(localVal); + + localValRef.current = localVal; + useEffect(()=>{ + return ()=>{ + /* When unmounted, trigger onRowChange */ + onRowChange({ ...row, [column.key]: localValRef.current?.trim()}, true); + }; + }, []); + + const onKeyDown = (e)=>{ + if(e.code === 'Tab' || e.code === 'Enter') { + e.preventDefault(); + onClose(); + } + }; + return ( + { + setLocalVal(e.target.value); + }} + onKeyDown={onKeyDown} + autoFocus + /> + ); +} + +FileNameEditor.propTypes = { + row: PropTypes.object, + column: PropTypes.object, + onRowChange: PropTypes.func, + onClose: PropTypes.func, +}; + +function CutomSortIcon({sortDirection}) { + if(sortDirection == 'DESC') { + return ; + } else if(sortDirection == 'ASC') { + return ; + } + return <>; +} +CutomSortIcon.propTypes = { + sortDirection: PropTypes.string, +}; + +export function CustomRow({inTest=false, ...props}) { + const gridUtils = useContext(GridContextUtils); + const handleKeyDown = (e)=>{ + if(e.code == 'Tab' || e.code == 'ArrowRight' || e.code == 'ArrowLeft') { + e.stopPropagation(); + } + if(e.code == 'Enter') { + gridUtils.onItemEnter(props.row); + } + }; + const isRowSelected = props.selectedCellIdx >= 0; + useEffect(()=>{ + if(isRowSelected) { + gridUtils.onItemSelect(props.rowIdx); + } + }, [props.selectedCellIdx]); + if(inTest) { + return
    ; + } + const onRowClick = (...args)=>{ + gridUtils.onItemClick?.(props.rowIdx); + props.onRowClick?.(...args); + }; + return ( + gridUtils.onItemEnter(row)} + selectCell={(row, column)=>props.selectCell(row, column)} aria-selected={isRowSelected}/> + ); +} +CustomRow.propTypes = { + inTest: PropTypes.bool, + row: PropTypes.object, + selectedCellIdx: PropTypes.number, + onRowClick: PropTypes.func, + rowIdx: PropTypes.number, + selectCell: PropTypes.func, +}; + +function FileNameFormatter({row}) { + const classes = useStyles(); + let icon = ; + if(row.file_type == 'dir') { + icon = ; + } else if(row.file_type == 'drive') { + icon = ; + } + return <> + {icon} + {Boolean(row.Protected) && } + {row['Filename']} + ; +} +FileNameFormatter.propTypes = { + row: PropTypes.object, +}; + +const columns = [ + { + key: 'Filename', + name: 'Name', + formatter: FileNameFormatter, + editor: FileNameEditor, + editorOptions: { + editOnClick: false, + onCellKeyDown: (e)=>e.preventDefault(), + } + },{ + key: 'Properties.DateModified', + name: 'Date Modified', + formatter: ({row})=><>{row.Properties?.['Date Modified']} + },{ + key: 'Properties.Size', + name: 'Size', + formatter: ({row})=><>{row.file_type != 'dir' && row.Properties?.['Size']} + } +]; + + +export default function ListView({items, operation, onItemSelect, onItemEnter, onItemClick, ...props}) { + const classes = useStyles(); + const gridRef = useRef(); + + useEffect(()=>{ + if(operation.type) { + operation.type == 'add' && gridRef.current.scrollToRow(operation.idx); + gridRef.current.selectCell({idx: 0, rowIdx: operation.idx}, true); + } + }, [operation]); + + useEffect(()=>{ + gridRef.current.selectCell({idx: 0, rowIdx: 0}); + }, [gridRef.current?.element]); + + return ( + + No files/folders found, + }} + onRowsChange={(rows)=>{ + operation?.onComplete?.(rows[operation.idx], operation.idx); + }} + {...props} + /> + + ); +} +ListView.propTypes = { + items: PropTypes.arrayOf(PropTypes.object), + operation: PropTypes.object, + onItemSelect: PropTypes.func, + onItemEnter: PropTypes.func, + onItemClick: PropTypes.func, +}; diff --git a/web/pgadmin/misc/file_manager/static/js/components/Uploader.jsx b/web/pgadmin/misc/file_manager/static/js/components/Uploader.jsx new file mode 100644 index 000000000..f03223424 --- /dev/null +++ b/web/pgadmin/misc/file_manager/static/js/components/Uploader.jsx @@ -0,0 +1,197 @@ +import React, { useCallback, useReducer, useEffect, useMemo } from 'react'; +import { Box, List, ListItem, makeStyles } from '@material-ui/core'; +import CloseIcon from '@material-ui/icons/CloseRounded'; +import { PgIconButton } from '../../../../../static/js/components/Buttons'; +import gettext from 'sources/gettext'; +import {useDropzone} from 'react-dropzone'; +import { FormFooterMessage, MESSAGE_TYPE } from '../../../../../static/js/components/FormComponents'; +import convert from 'convert-units'; +import _ from 'lodash'; +import PropTypes from 'prop-types'; + +const useStyles = makeStyles((theme)=>({ + root: { + position: 'absolute', + top: 0, + bottom: 0, + left: 0, + right: 0, + zIndex: 1, + backgroundColor: theme.palette.background.default, + display: 'flex', + flexDirection: 'column', + padding: '4px', + }, + uploadArea: { + border: `1px dashed ${theme.palette.grey[600]}`, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + flexGrow: 1, + flexDirection: 'column', + cursor: 'move', + textAlign: 'center', + padding: '4px', + }, + uploadFilesRoot: { + width: '350px', + border: `1px dashed ${theme.palette.grey[600]}`, + borderLeft: 'none', + overflowX: 'hidden', + overflowY: 'auto' + }, + uploadProgress: { + position: 'unset', + padding: 0, + }, + uploadPending: { + + } +})); + +export function filesReducer(state, action) { + let newState = [...state]; + switch (action.type) { + case 'add': + newState.unshift(...action.files.map((file)=>({ + id: _.uniqueId('f'), + file: file, + progress: 0, + started: false, + failed: false, + done: false, + deleting: false, + }))); + break; + case 'started': + _.find(newState, (f)=>f.id==action.id).started = true; + break; + case 'progress': + _.find(newState, (f)=>f.id==action.id).progress = action.value; + break; + case 'failed': + _.find(newState, (f)=>f.id==action.id).failed = true; + break; + case 'done': + _.find(newState, (f)=>f.id==action.id).done = true; + break; + case 'remove': + newState = newState.filter((f)=>f.id!=action.id) || []; + break; + default: + break; + } + return newState; +} + +export function getFileSize(bytes) { + let conVal = convert(bytes).from('B').toBest(); + conVal.val = Math.round(conVal.val * 100) / 100; + return `${conVal.val} ${conVal.unit}`; +} + +export function UploadedFile({upfile, removeFile}) { + let type = MESSAGE_TYPE.INFO; + let message = `Uploading... ${upfile.progress?.toString() || ''}%`; + if(upfile.done) { + type = MESSAGE_TYPE.SUCCESS; + message = 'Uploaded!'; + } else if(upfile.failed) { + type = MESSAGE_TYPE.ERROR; + message = 'Failed!'; + } + + return ( + + + {upfile.file.name} + + } size="xs" noBorder onClick={removeFile} /> + + + {useMemo(()=>getFileSize(upfile.file.size), [])} + + + ); +} +UploadedFile.propTypes = { + upfile: PropTypes.object, + removeFile: PropTypes.func, +}; + + +export default function Uploader({fmUtilsObj, onClose}) { + const classes = useStyles(); + const [files, dispatchFileAction] = useReducer(filesReducer, []); + const onDrop = useCallback(acceptedFiles => { + dispatchFileAction({ + type: 'add', + files: acceptedFiles, + }); + }, []); + const {getRootProps, getInputProps} = useDropzone({onDrop}); + + useEffect(()=>{ + files.forEach(async (upfile)=>{ + if(!upfile.started && !upfile.failed) { + try { + dispatchFileAction({ + type: 'started', + id: upfile.id, + }); + await fmUtilsObj.uploadItem(upfile.file, (progressEvent)=>{ + const {loaded, total} = progressEvent; + const percent = Math.floor((loaded * 100) / total); + dispatchFileAction({ + type: 'progress', + id: upfile.id, + value: percent, + }); + }); + dispatchFileAction({ + type: 'done', + id: upfile.id, + }); + } catch { + dispatchFileAction({ + type: 'failed', + id: upfile.id, + }); + } + } + }); + }, [files.length]); + + return ( + + + } size="xs" noBorder onClick={onClose} /> + + + + + {gettext('Drop files here, or click to select files.')} + {gettext('The file size limit (per file) is %s MB.', fmUtilsObj.config?.upload?.fileSizeLimit)} + + {files.length > 0 && + + + {files.map((upfile)=>( + { + dispatchFileAction({ + type: 'remove', + id: upfile.id, + }); + }}/> + ))} + + } + + + ); +} +Uploader.propTypes = { + fmUtilsObj: PropTypes.object, + onClose: PropTypes.func, +}; diff --git a/web/pgadmin/misc/file_manager/static/js/create_dialogue.js b/web/pgadmin/misc/file_manager/static/js/create_dialogue.js deleted file mode 100644 index cdd9917e1..000000000 --- a/web/pgadmin/misc/file_manager/static/js/create_dialogue.js +++ /dev/null @@ -1,195 +0,0 @@ -///////////////////////////////////////////////////////////// -// -// pgAdmin 4 - PostgreSQL Tools -// -// Copyright (C) 2013 - 2022, The pgAdmin Development Team -// This software is released under the PostgreSQL Licence -// -////////////////////////////////////////////////////////////// - -import gettext from 'sources/gettext'; -import url_for from 'sources/url_for'; -import $ from 'jquery'; -import Alertify from 'pgadmin.alertifyjs'; -import pgAdmin from 'sources/pgadmin'; -import {removeTransId, set_last_traversed_dir} from './helpers'; -import Notify from '../../../../static/js/helpers/Notifier'; - -// Declare the Create mode dialog -module.exports = Alertify.dialog('createModeDlg', function() { - // Dialog property - return { - setup: function() { - return { - buttons: [{ - text: gettext('Cancel'), - key: 27, - className: 'btn btn-secondary fa fa-times file_manager_create_cancel pg-alertify-button', - },{ - text: gettext('Create'), - key: 13, - className: 'btn btn-primary fa fa-file file_manager_create file_manager_ok pg-alertify-button disabled', - }], - options: { - closableByDimmer: false, - maximizable: false, - closable: false, - movable: true, - padding: !1, - overflow: !1, - model: 0, - resizable: true, - pinnable: false, - modal: false, - autoReset: false, - }, - }; - }, - replace_file: function() { - var $yesBtn = $('.replace_file .btn_yes'), - $noBtn = $('.replace_file .btn_no'); - - $('.storage_dialog #uploader .input-path').attr('disabled', true); - $('.file_manager_ok').addClass('disabled'); - $('.replace_file, .fm_dimmer').show(); - - $yesBtn.on('click',() => { - $('.replace_file, .fm_dimmer').hide(); - $yesBtn.off(); - $noBtn.off(); - var newFile = $('.storage_dialog #uploader .input-path').val(); - - pgAdmin.Browser.Events.trigger('pgadmin-storage:finish_btn:create_file', newFile); - $('.file_manager_create_cancel').trigger('click'); - $('.storage_dialog #uploader .input-path').attr('disabled', false); - $('.file_manager_ok').removeClass('disabled'); - }); - - $noBtn.on('click',() => { - $('.replace_file, .fm_dimmer').hide(); - $yesBtn.off(); - $noBtn.off(); - $('.storage_dialog #uploader .input-path').attr('disabled', false); - $('.file_manager_ok').removeClass('disabled'); - }); - }, - is_file_exist: function() { - var full_path = $('.storage_dialog #uploader .input-path').val(), - path = full_path.substr(0, full_path.lastIndexOf('/') + 1), - selected_item = full_path.substr(full_path.lastIndexOf('/') + 1), - is_exist = false; - - var file_data = { - 'path': path, - 'name': selected_item, - 'mode': 'is_file_exist', - }; - - $.ajax({ - type: 'POST', - data: JSON.stringify(file_data), - url: url_for('file_manager.filemanager', { - 'trans_id': this.trans_id, - }), - dataType: 'json', - contentType: 'application/x-download; charset=utf-8', - async: false, - }) - .done(function(resp) { - var data = resp.data.result; - if (data['Code'] === 1) { - is_exist = true; - } else { - is_exist = false; - } - }); - return is_exist; - }, - check_permission: function(path) { - var permission = false, - post_data = { - 'path': path, - 'mode': 'permission', - }; - - $.ajax({ - type: 'POST', - data: JSON.stringify(post_data), - url: url_for('file_manager.filemanager', { - 'trans_id': this.trans_id, - }), - dataType: 'json', - contentType: 'application/json; charset=utf-8', - async: false, - }) - .done(function(resp) { - var data = resp.data.result; - if (data.Code === 1) { - permission = true; - } else { - $('.file_manager_ok').addClass('disabled'); - Notify.error(data.Error); - } - }) - .fail(function() { - $('.file_manager_ok').addClass('disabled'); - Notify.error(gettext('Error occurred while checking access permission.')); - }); - return permission; - }, - callback: function(closeEvent) { - closeEvent.cancel = false; - if (closeEvent.button.text == gettext('Create')) { - var act_variable = document.activeElement.id; - if(act_variable != 'refresh_list') { - var newFile = $('.storage_dialog #uploader .input-path').val(), - file_data = { - 'path': $('.currentpath').val(), - }, - innerbody, - ext = $('.allowed_file_types select').val(); - - /* - Add the file extension if necessary, and if the file type selector - isn't set to "All Files". If there's no . at all in the path, or - there is a . already but it's not following the last /, AND the - extension isn't *, then we add the extension. - */ - if ((!newFile.includes('.') || - newFile.split('.').pop() != ext) && - ext != '*') { - newFile = newFile + '.' + ext; - $('.storage_dialog #uploader .input-path').val(newFile); - } - - if (!this.check_permission(newFile)) { - closeEvent.cancel = true; - return; - } - - if (!_.isUndefined(newFile) && newFile !== '' && this.is_file_exist()) { - this.replace_file(); - this.$container.find('.replace_file').find('.btn_yes').trigger('focus'); - closeEvent.cancel = true; - } else { - pgAdmin.Browser.Events.trigger('pgadmin-storage:finish_btn:create_file', newFile); - innerbody = $(this.elements.body).find('.storage_content'); - $(innerbody).find('*').off(); - innerbody.remove(); - removeTransId(this.trans_id); - } - - set_last_traversed_dir(file_data, this.trans_id); - } else { - closeEvent.cancel = true; - } - } else if (closeEvent.button.text == gettext('Cancel')) { - innerbody = $(this.elements.body).find('.storage_content'); - $(innerbody).find('*').off(); - innerbody.remove(); - removeTransId(this.trans_id); - pgAdmin.Browser.Events.trigger('pgadmin-storage:cancel_btn:create_file'); - } - }, - }; -}, false, 'fileSelectionDlg'); diff --git a/web/pgadmin/misc/file_manager/static/js/file_manager.js b/web/pgadmin/misc/file_manager/static/js/file_manager.js deleted file mode 100644 index 0b4a33cc1..000000000 --- a/web/pgadmin/misc/file_manager/static/js/file_manager.js +++ /dev/null @@ -1,55 +0,0 @@ -///////////////////////////////////////////////////////////// -// -// pgAdmin 4 - PostgreSQL Tools -// -// Copyright (C) 2013 - 2022, The pgAdmin Development Team -// This software is released under the PostgreSQL Licence -// -////////////////////////////////////////////////////////////// - -import './select_dialogue'; -import './create_dialogue'; -import './storage_dialogue'; - -define('misc.file_manager', [ - 'sources/gettext', 'sources/url_for', 'jquery', 'underscore', - 'sources/pgadmin', 'pgadmin.alertifyjs', -], function(gettext, url_for, $, _, pgAdmin, Alertify) { - pgAdmin = pgAdmin || window.pgAdmin || {}; - - /* - * - * - * Hmm... this module is already been initialized, we can refer to the old - * object from here. - */ - if (pgAdmin.FileManager) { - return pgAdmin.FileManager; - } - - pgAdmin.FileManager = { - init: function() { - if (this.initialized) { - return; - } - - this.initialized = true; - - }, - // Call dialogs subject to dialog_type param - show_dialog: function(params) { - let dialogWidth = pgAdmin.Browser.stdW.calc(pgAdmin.Browser.stdW.md); - let dialogHeight = pgAdmin.Browser.stdH.calc(pgAdmin.Browser.stdH.lg); - if (params.dialog_type == 'create_file') { - Alertify.createModeDlg(params).resizeTo(dialogWidth, dialogHeight); - } else if(params.dialog_type == 'storage_dialog') { - Alertify.fileStorageDlg(params).resizeTo(dialogWidth, dialogHeight); - } - else { - Alertify.fileSelectionDlg(params).resizeTo(dialogWidth, dialogHeight); - } - }, - }; - - return pgAdmin.FileManager; -}); diff --git a/web/pgadmin/misc/file_manager/static/js/helpers.js b/web/pgadmin/misc/file_manager/static/js/helpers.js deleted file mode 100644 index af24bfa71..000000000 --- a/web/pgadmin/misc/file_manager/static/js/helpers.js +++ /dev/null @@ -1,47 +0,0 @@ -///////////////////////////////////////////////////////////// -// -// pgAdmin 4 - PostgreSQL Tools -// -// Copyright (C) 2013 - 2022, The pgAdmin Development Team -// This software is released under the PostgreSQL Licence -// -////////////////////////////////////////////////////////////// - -import url_for from 'sources/url_for'; -import $ from 'jquery'; - -// Send a request to get transaction id -export function getTransId(configs) { - return $.ajax({ - data: configs, - type: 'POST', - async: false, - url: url_for('file_manager.get_trans_id'), - dataType: 'json', - contentType: 'application/json; charset=utf-8', - }); -} - -// Function to remove trans id from session -export function removeTransId(trans_id) { - return $.ajax({ - type: 'GET', - async: false, - url: url_for('file_manager.delete_trans_id', { - 'trans_id': trans_id, - }), - dataType: 'json', - contentType: 'application/json; charset=utf-8', - }); -} - -export function set_last_traversed_dir(path, trans_id) { - return $.ajax({ - url: url_for('file_manager.save_last_dir', { - 'trans_id': trans_id, - }), - type: 'POST', - data: JSON.stringify(path), - contentType: 'application/json', - }); -} diff --git a/web/pgadmin/misc/file_manager/static/js/index.js b/web/pgadmin/misc/file_manager/static/js/index.js new file mode 100644 index 000000000..63ddc7629 --- /dev/null +++ b/web/pgadmin/misc/file_manager/static/js/index.js @@ -0,0 +1,25 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2022, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// +import pgAdmin from 'sources/pgadmin'; +import FileManagerModule from './FileManagerModule'; + +/* eslint-disable */ +/* This is used to change publicPath of webpack at runtime for loading chunks */ +/* Do not add let, var, const to this variable */ +__webpack_public_path__ = window.resourceBasePath; +/* eslint-enable */ + +if(!pgAdmin.Tools) { + pgAdmin.Tools = {}; +} +pgAdmin.Tools.FileManager = FileManagerModule.getInstance(pgAdmin); + +module.exports = { + FileManager: pgAdmin.Tools.FileManager, +}; diff --git a/web/pgadmin/misc/file_manager/static/js/select_dialogue.js b/web/pgadmin/misc/file_manager/static/js/select_dialogue.js deleted file mode 100644 index 74903e1cd..000000000 --- a/web/pgadmin/misc/file_manager/static/js/select_dialogue.js +++ /dev/null @@ -1,148 +0,0 @@ -///////////////////////////////////////////////////////////// -// -// pgAdmin 4 - PostgreSQL Tools -// -// Copyright (C) 2013 - 2022, The pgAdmin Development Team -// This software is released under the PostgreSQL Licence -// -////////////////////////////////////////////////////////////// - -import gettext from 'sources/gettext'; -import url_for from 'sources/url_for'; -import $ from 'jquery'; -import Alertify from 'pgadmin.alertifyjs'; -import pgAdmin from 'sources/pgadmin'; -import {getTransId, removeTransId, set_last_traversed_dir} from './helpers'; - -// Declare the Selection dialog -module.exports = Alertify.dialog('fileSelectionDlg', function() { - // Dialog property - return { - main: function(params) { - // Set title and button name - var self = this; - if (_.isUndefined(params['dialog_title'])) { - params['dialog_title'] = gettext('Select file'); - } - self.dialog_type = params['dialog_type']; - - this.set('title', params['dialog_title']); - this.params = JSON.stringify(params); - - this.show(); - - }, - settings: { - label: undefined, - }, - settingUpdated: function(key, oldValue, newValue) { - switch (key) { - case 'message': - this.setMessage(newValue); - break; - case 'label': - if (this.__internal.buttons[0].element) { - this.__internal.buttons[0].element.innerHTML = newValue; - } - break; - default: - break; - } - }, - prepare: function() { - var self = this; - - self.$container.find('.storage_content').remove(); - self.$container.append('
    '); - - var content = self.$container.find('.storage_content'); - content.empty(); - - // Add our class to alertify - $(this.elements.body.childNodes[0]).addClass('alertify_tools_dialog_properties'); - $(this.elements.root).css('z-index', 3002); - $.get(url_for('file_manager.index'), function(data) { - content.append(data); - }); - - var transId = getTransId(self.params); - var t_res; - if (transId.readyState == 4) { - t_res = JSON.parse(transId.responseText); - } - self.trans_id = _.isUndefined(t_res) ? 0 : t_res.data.fileTransId; - - setTimeout(function() { - $(self.$container.find('.file_manager')).on('enter-key', function() { - $($(self.elements.footer).find('.file_manager_ok')).trigger('click'); - }); - }, 200); - if(self.__internal.buttons[1]) - self.__internal.buttons[1].element.disabled = true; - }, - setup: function() { - return { - buttons: [{ - text: gettext('Cancel'), - key: 27, - className: 'btn btn-secondary fa fa-times pg-alertify-button', - },{ - text: gettext('Select'), - key: 13, - className: 'btn btn-primary fa fa-file file_manager_ok pg-alertify-button disabled', - }], - options: { - closableByDimmer: false, - maximizable: false, - closable: false, - movable: true, - padding: !1, - overflow: !1, - model: 0, - resizable: true, - pinnable: false, - modal: false, - autoReset: false, - }, - }; - }, - callback: function(closeEvent) { - var innerbody; - closeEvent.cancel = false; - if (closeEvent.button.text == gettext('Select')) { - var act_variable = document.activeElement.id; - if(act_variable !='refresh_list') { - var newFile = $('.storage_dialog #uploader .input-path').val(), - file_data = { - 'path': $('.currentpath').val(), - }; - - pgAdmin.Browser.Events.trigger('pgadmin-storage:finish_btn:' + this.dialog_type, newFile); - innerbody = $(this.elements.body).find('.storage_content'); - $(innerbody).find('*').off(); - innerbody.remove(); - removeTransId(this.trans_id); - // Ajax call to store the last directory visited once user press select button - - set_last_traversed_dir(file_data, this.trans_id); - } else { - closeEvent.cancel = true; - } - } else if (closeEvent.button.text == gettext('Cancel')) { - innerbody = $(this.elements.body).find('.storage_content'); - $(innerbody).find('*').off(); - innerbody.remove(); - removeTransId(this.trans_id); - pgAdmin.Browser.Events.trigger('pgadmin-storage:cancel_btn:' + this.dialog_type); - } - }, - build: function() { - this.$container = $('
    '); - this.elements.content.appendChild(this.$container.get(0)); - Alertify.pgDialogBuild.apply(this); - }, - hooks: { - onshow: function() {/* This is intentional (SonarQube) */}, - }, - }; -}); diff --git a/web/pgadmin/misc/file_manager/static/js/storage_dialogue.js b/web/pgadmin/misc/file_manager/static/js/storage_dialogue.js deleted file mode 100644 index 38f01f599..000000000 --- a/web/pgadmin/misc/file_manager/static/js/storage_dialogue.js +++ /dev/null @@ -1,45 +0,0 @@ -///////////////////////////////////////////////////////////// -// -// pgAdmin 4 - PostgreSQL Tools -// -// Copyright (C) 2013 - 2022, The pgAdmin Development Team -// This software is released under the PostgreSQL Licence -// -////////////////////////////////////////////////////////////// - -import gettext from 'sources/gettext'; -import Alertify from 'pgadmin.alertifyjs'; - -// Declare the Storage dialog -module.exports = Alertify.dialog('fileStorageDlg', function() { - // Dialog property - return { - settingUpdated: function(key, oldValue, newValue) { - if(key == 'message') { - this.setMessage(newValue); - } - }, - setup: function() { - return { - buttons: [{ - text: gettext('Cancel'), - key: 27, - className: 'btn btn-secondary fa fa-times pg-alertify-button', - }], - options: { - closableByDimmer: false, - maximizable: false, - closable: false, - movable: true, - padding: !1, - overflow: !1, - model: 0, - resizable: true, - pinnable: false, - modal: false, - autoReset: false, - }, - }; - }, - }; -}, true, 'fileSelectionDlg'); diff --git a/web/pgadmin/misc/file_manager/static/js/utility.js b/web/pgadmin/misc/file_manager/static/js/utility.js deleted file mode 100644 index 47bd36f8c..000000000 --- a/web/pgadmin/misc/file_manager/static/js/utility.js +++ /dev/null @@ -1,2008 +0,0 @@ -///////////////////////////////////////////////////////////// -// -// pgAdmin 4 - PostgreSQL Tools -// -// Copyright (C) 2013 - 2022, The pgAdmin Development Team -// This software is released under the PostgreSQL Licence -// -////////////////////////////////////////////////////////////// - -/** - * Filemanager JS core - * - * filemanager.js - * - * @license MIT License - * @author Jason Huck - Core Five Labs - * @author Simon Georget - * @copyright Authors - */ -import Notify from '../../../../static/js/helpers/Notifier'; - -define([ - 'jquery', 'underscore', - 'sources/gettext', 'sources/url_for', 'dropzone', 'sources/pgadmin', - 'sources/csrf', 'tablesorter', 'tablesorter-metric', -], function($, _, gettext, url_for, Dropzone, pgAdmin, csrf) { - - pgAdmin.Browser = pgAdmin.Browser || {}; - - /*--------------------------------------------------------- - Define functions used for various operations - ---------------------------------------------------------*/ - // Set the CSRF Token - csrf.setPGCSRFToken(pgAdmin.csrf_token_header, pgAdmin.csrf_token); - - // Return file extension - var getFileExtension = function(name) { - var found = name.lastIndexOf('.') + 1; - return (found > 0 ? name.substr(found) : ''); - }; - - /* Common function to load: - * en.json language file - * file_manager_config.js config file - * return transaction id - */ - var loadData = function(url) { - return $.ajax({ - async: false, - cache: false, - url: url, - dataType: 'json', - contentType: 'application/json; charset=utf-8', - }); - }; - - var getFileFormat = function(data) { - // Get last selected file format - return $.ajax({ - async: false, - cache: false, - url: url_for('settings.get_file_format_setting'), - data : $.extend({}, data), - dataType: 'json', - contentType: 'application/json; charset=utf-8', - }); - }; - - // Set enable/disable state of list and grid view - var setViewButtonsFor = function(viewMode) { - if (viewMode == 'grid') { - $('.grid').addClass('ON'); - $('.list').removeClass('ON'); - } else { - $('.list').addClass('ON'); - $('.grid').removeClass('ON'); - } - }; - - var save_file_dialog_view = function(view, trans_id) { - return $.ajax({ - url: url_for('file_manager.save_file_dialog_view', { - 'trans_id': trans_id, - }), - type: 'POST', - async: true, - data: JSON.stringify({ - 'view': view, - }), - contentType: 'application/json', - }); - }; - - var save_show_hidden_file_option = function(option, trans_id) { - return $.ajax({ - url: url_for('file_manager.save_show_hidden_file_option', { - 'trans_id': trans_id, - }), - type: 'PUT', - async: true, - data: JSON.stringify({ - 'show_hidden': option, - }), - contentType: 'application/json', - }); - }; - - // nameFormat (), separate filename from extension - var nameFormat = function(input) { - var filename = ''; - if (input.lastIndexOf('.') != -1) { - filename = input.substr(0, input.lastIndexOf('.')); - filename += '.' + input.split('.').pop(); - } else { - filename = input; - } - return filename; - }; - - /* - * Test if Data structure has the 'cap' capability - * 'cap' is one of 'select', 'rename', 'delete', 'download' - */ - var has_capability = function(data, cap) { - if (typeof(data.Capabilities) == 'undefined') { - return true; - } else { - return ($.inArray(cap, data.Capabilities) > -1); - } - }; - - // return filename extension - var getExtension = function(filename) { - if (filename.split('.').length == 1) { - return ''; - } - return filename.split('.').pop(); - }; - - /* - * Binds specific actions to the toolbar based on capability. - * and show/hide buttons - */ - var bindToolbar = function(data) { - - // hide/show rename, upload and create button - if (_.has(data, 'Capabilities')) { - _.each(data.Capabilities, function(cap) { - var target_btn = 'button.' + cap, - $target_el = $('.file_manager').find(target_btn); - if (!has_capability(data, cap) || pgAdmin.FileUtils.hideButtons()) { - $target_el.hide(); - } else { - $target_el.show(); - } - }); - } - - if (!has_capability(data, 'delete') || pgAdmin.FileUtils.hideButtons()) { - $('.file_manager').find('button.delete').hide(); - } else { - $('.file_manager').find('button.delete').on('click',() => { - // hide dimmer - $('.fileinfo .delete_item, .fm_dimmer').show(); - }); - - // take action based on pressed button yes or no - $('.fileinfo .delete_item button.btn_yes').off().on('click', function() { - var path; - if ($('.fileinfo').data('view') == 'grid') { - path = decodeURI($('.fileinfo').find('#contents li.selected .clip span').attr('data-alt')); - if (path.lastIndexOf('/') == path.length - 1) { - data.Path = path; - deleteItem(data); - } else { - deleteItem(data); - } - } else { - path = $('.fileinfo').find('table#contents tbody tr.selected td:first-child').attr('title'); - if (path.lastIndexOf('/') == path.length - 1) { - data.Path = path; - deleteItem(data); - } else { - deleteItem(data); - } - } - // hide dimmer - $('.fileinfo .fm_dimmer').hide(); - }); - - } - - // Download file on download button click - if (!has_capability(data, 'download') || pgAdmin.FileUtils.hideButtons()) { - $('.file_manager').find('button.download').hide(); - } else { - $('.file_manager').find('button.download').off().on('click', function() { - var path, - params = {}; - - params[pgAdmin.csrf_token_header] = pgAdmin.csrf_token; - - if ($('.fileinfo').data('view') == 'grid') { - path = $('.fileinfo li.selected').find('.clip span').attr('data-alt'); - } else { - path = $('.fileinfo').find('table#contents tbody tr.selected td:first-child').attr('title'); - } - download_file(path); - }); - } - }; - - // enable/disable button when files/folder are loaded - var enable_disable_btn = function() { - if ($('.fileinfo').data('view') == 'grid') { - var $grid_file = $('.file_manager').find('#contents li.selected'); - $grid_file.removeClass('selected'); - $('.file_manager').find('button.delete').prop('disabled', true); - $('.file_manager').find('button.download').prop('disabled', true); - $('.file_manager').find('button.rename').prop('disabled', true); - if ($grid_file.length > 0) { - $('.file_manager_ok').addClass('disabled'); - $('.file_manager_ok').attr('disabled', true); - } - } else { - var $list_file = $('.fileinfo').find('table#contents tbody tr.selected'); - $list_file.removeClass('selected'); - $('.file_manager').find('button.delete').prop('disabled', true); - $('.file_manager').find('button.download').prop('disabled', true); - $('.file_manager').find('button.rename').prop('disabled', true); - if ($list_file.length > 0) { - $('.file_manager_ok').addClass('disabled'); - $('.file_manager_ok').attr('disabled', true); - } - } - - $('.delete_item').hide(); - // clear address bar - $('.file_manager #uploader .input-path').show(); - $('.file_manager #uploader .show_selected_file').remove(); - }; - - /* - * Rename the current item and returns the new name. - * by double clicking or by clicking the "Rename" button in - * table(list) views. - */ - var renameItem = function(data) { - var finalName = '', - lg = pgAdmin.FileUtils.lg, - getNewName = function(rname) { - if (rname !== '') { - var givenName = nameFormat(rname), - suffix = getExtension(data.Filename); - if (suffix.length > 0) { - givenName = givenName + '.' + suffix; - } - - var oldPath = data.Path, - post_data = { - 'mode': 'rename', - 'old': data.Path, - 'new': givenName, - }; - - $.ajax({ - type: 'POST', - data: JSON.stringify(post_data), - url: pgAdmin.FileUtils.fileConnector, - dataType: 'json', - contentType: 'application/json; charset=utf-8', - async: false, - }) - .done(function(resp) { - var result = resp.data.result; - if (result.Code === 1) { - var newPath = result['New Path'], - newName = result['New Name'], - title = $('#preview h1').attr('title'); - - if (typeof title != 'undefined' && title == oldPath) { - $('#preview h1').text(newName); - } - - if ($('.fileinfo').data('view') == 'grid') { - $('.fileinfo span[data-alt="' + oldPath + '"]').parent().next('div span').text(newName); - $('.fileinfo span[data-alt="' + oldPath + '"]').attr('data-alt', newPath); - } else { - $('.fileinfo td[title="' + oldPath + '"]').text(newName); - $('.fileinfo td[title="' + oldPath + '"]').attr('title', newPath); - } - $('#preview h1').html(newName); - - // actualized data for binding - data.Path = newPath; - data.Filename = newName; - - // UnBind toolbar functions. - $('.fileinfo').find('button.rename, button.delete, button.download').off(); - - Notify.success(lg.successful_rename); - } else { - Notify.error(result.Error); - } - - finalName = result['New Name']; - }); - } - }; - - getNewName(data.NewFilename); - return finalName; - }; - - /* - * delete the folder or files by clicking the "Delete" button - * in table(list) view - */ - var deleteItem = function(data) { - var isDeleted = false, - lg = pgAdmin.FileUtils.lg; - - var doDelete = function(sel_data) { - var post_data = { - 'mode': 'delete', - 'path': sel_data.Path, - }; - - $.ajax({ - type: 'POST', - data: JSON.stringify(post_data), - url: pgAdmin.FileUtils.fileConnector, - dataType: 'json', - contentType: 'application/json; charset=utf-8', - async: false, - }) - .done(function(resp) { - var result = resp.data.result; - if (result.Code === 1) { - isDeleted = true; - if (isDeleted) { - Notify.success(lg.successful_delete); - var rootpath = result.Path.substring(0, result.Path.length - 1); // removing the last slash - rootpath = rootpath.substr(0, rootpath.lastIndexOf('/') + 1); - getFolderInfo(rootpath); - } - } else { - isDeleted = false; - Notify.error(result.Error); - } - }); - return isDeleted; - }; - - doDelete(data); - return isDeleted; - }; - - /*--------------------------------------------------------- - Functions to Retrieve File and Folder Details - ---------------------------------------------------------*/ - /* - * Retrieves information about the specified file as a JSON - * object and uses that data to populate a template for - * list views. Binds the toolbar for that file/folder to - * enable specific actions. Called whenever an item is - * clicked in list views. - */ - var getFileInfo = function(file) { - // Update location for status, upload, & new folder functions. - pgAdmin.FileUtils.setUploader(file); - var capabilities = pgAdmin.FileUtils.data.capabilities, - is_file_valid = false, - post_data = { - 'path': file, - 'mode': 'getinfo', - }; - - // Retrieve the data & populate the template. - $.ajax({ - type: 'POST', - data: JSON.stringify(post_data), - url: pgAdmin.FileUtils.fileConnector, - dataType: 'json', - contentType: 'application/json; charset=utf-8', - async: false, - }) - .done(function(resp) { - var data = resp.data.result; - if (data.Code === 1) { - $('.file_manager_ok').removeClass('disabled'); - $('.file_manager_ok').attr('disabled', false); - data.Capabilities = capabilities; - bindToolbar(data); - if (data.FileType == 'Directory') { - // Enable/Disable level up button - enab_dis_level_up(); - $('.file_manager_ok').addClass('disabled'); - $('.file_manager_ok').attr('disabled', true); - $('.file_manager button.delete, .file_manager button.rename').attr('disabled', 'disabled'); - $('.file_manager button.download').attr('disabled', 'disabled'); - - if (file.charAt(file.length - 1) != '/' && file.charAt(file.length - 1) != '\\') { - file += '/'; - } - getFolderInfo(file); - } else { - is_file_valid = true; - } - } else { - $('.file_manager_ok').addClass('disabled'); - $('.file_manager_ok').attr('disabled', true); - Notify.error(data.Error); - } - - // Disable select button if user select file. - if (pgAdmin.FileUtils.data.Capabilities.includes('select_folder')) { - $('.file_manager_ok').addClass('disabled'); - $('.file_manager_ok').attr('disabled', true); - } - - }); - return is_file_valid; - }; - - var checkPermission = function(path) { - var permission = false, - post_data = { - 'path': path, - 'mode': 'permission', - }; - - $.ajax({ - type: 'POST', - data: JSON.stringify(post_data), - url: pgAdmin.FileUtils.fileConnector, - dataType: 'json', - contentType: 'application/json; charset=utf-8', - async: false, - }) - .done(function(resp) { - var data = resp.data.result; - if (data.Code === 1) { - permission = true; - } else { - $('.file_manager_ok').addClass('disabled'); - $('.file_manager_ok').attr('disabled', true); - Notify.error(data.Error); - } - }) - .fail(function() { - $('.file_manager_ok').addClass('disabled'); - $('.file_manager_ok').attr('disabled', true); - Notify.error( - gettext('Error occurred while checking access permission.') - ); - }); - return permission; - }; - - var getNoDataView = function(data) { - var lg = pgAdmin.FileUtils.lg; - var cap_no_folders = ['upload', 'create']; - data.Capabilities = cap_no_folders; - bindToolbar(data); - - return `
    ${lg.could_not_retrieve_folder}
    `; - }; - - var getCapClasses = function(item_data, capabilities) { - let capClasses = ''; - capClasses = Object.keys(capabilities).map(function(cap) { - if (has_capability(item_data, capabilities[cap])) { - return 'cap_' + capabilities[cap]; - } - }).join(' '); - return capClasses; - }; - - var getGridView = function(data, capabilities) { - let ret_ele = '
      ', - no_data = _.isEmpty(data); - - if(!no_data) { - ret_ele += Object.keys(data).sort(function keyOrder(x, y) { - return pgAdmin.natural_sort(x.toLowerCase(), y.toLowerCase()); - }).map(function(key) { - let item_data = data[key], - props = item_data.Properties, - filename = _.escape(item_data.Filename), - icon_type = '', - cap_classes = ''; - - cap_classes = getCapClasses(item_data, capabilities); - - item_data.Capabilities = capabilities; - bindToolbar(item_data); - - if (item_data.file_type == 'dir') { - icon_type = 'fa fa-folder-open fm_folder_grid'; - props.Size = ''; - } else if (item_data.file_type == 'drive') { - icon_type = 'fa fa-hdd fm_drive'; - props.Size = ''; - } else { - icon_type = 'fa fa-file-alt fm_file_grid'; - } - - /* For the html ele */ - let item_ele = - `
    • -
      - `; - - if (item_data.Protected == 1) { - item_ele += ''; - } - - item_ele += '
      '; - - if (!has_capability(item_data, 'rename')) { - item_ele += `${filename}`; - } else { - item_ele += - `
      - - ${filename} -
      `; - } - if (props.Width && props.Width != '') { - item_ele += `${props.Width}x${props.Height}`; - } - if (props.Size && props.Size != '') { - item_ele += `${props.Size}`; - } - if (props['Date Created'] && props['Date Created'] != '') { - item_ele += `${props['Date Created']}`; - } - if (props['Date Modified'] && props['Date Modified'] != '') { - item_ele += `${props['Date Modified']}`; - } - item_ele += '
    • '; - - return item_ele; - }).join('\n'); - } - - ret_ele += '
    '; - - if(no_data) { - ret_ele += getNoDataView(data); - } - return ret_ele; - }; - - var getListView = function(data, capabilities) { - let lg = pgAdmin.FileUtils.lg; - let no_data = _.isEmpty(data); - - /* file_listing_table class makes height 100%, because of which No folder message is not displayed - * file_listing_table_no_data will be removed when new folder is created - */ - let ret_ele = - ` - - - - - - - - `; - - if(!no_data) { - ret_ele += Object.keys(data).sort(function keyOrder(x, y) { - return pgAdmin.natural_sort(x.toLowerCase(), y.toLowerCase()); - }).map(function(key) { - let item_data = data[key], - props = item_data.Properties, - icon_type = '', - class_type = '', - cap_classes = ''; - - cap_classes = getCapClasses(item_data, capabilities); - - item_data.Capabilities = capabilities; - bindToolbar(item_data); - - if (item_data.file_type == 'dir') { - class_type = 'tbl_folder'; - icon_type = 'fa fa-folder-open fm_folder_list'; - props.Size = ''; - } else if (item_data.file_type == 'drive') { - class_type = 'tbl_drive'; - icon_type = 'fa fa-hdd'; - props.Size = ''; - } else { - class_type = 'tbl_file'; - icon_type = 'fa fa-file-alt'; - } - - /* For the html ele */ - let item_ele = - ` - '; - if (props.Size && props.Size != '') { - item_ele += ``; - } else { - item_ele += ''; - } - - if (props['Date Modified'] && props['Date Modified'] != '') { - item_ele += ``; - } else { - item_ele += ''; - } - - item_ele += ''; - - return item_ele; - }).join('\n'); - } - - ret_ele += - ` -
    - ${lg.name} - - ${lg.size} - - ${lg.modified} -
    `; - - let data_protected = ''; - if (item_data.Protected == 1) { - data_protected = ''; - } - if (!has_capability(data[key], 'rename')) { - item_ele += - `${data_protected}; - ${_.escape(item_data.Filename)}`; - } else { - item_ele += - `
    - -
    -
    - - ${data_protected} - ${_.escape(item_data.Filename)} -
    -
    -
    `; - } - item_ele += '
    ${props.Size}${props['Date Modified']}
    `; - - if(no_data) { - ret_ele += getNoDataView(data); - } - return ret_ele; - }; - - var enableSelect = function() { - // Enable select button if user select folder. - if (pgAdmin.FileUtils.data && pgAdmin.FileUtils.data.Capabilities.includes('select_folder')) { - $('.file_manager_ok').removeClass('disabled'); - $('.file_manager_ok').attr('disabled', false); - } - }; - - var disableSelect = function() { - // Disable select button if user select file. - $('.file_manager_ok').addClass('disabled'); - $('.file_manager_ok').attr('disabled', true); - }; - - /* - * Retrieves data for all items within the given folder and - * creates a list view. - */ - var getFolderInfo = function(path, file_type, user_input) { - $('.storage_dialog #uploader .input-path').prop('disabled', true); - if (!file_type) { - file_type = ''; - } - var capabilities = pgAdmin.FileUtils.data.Capabilities; - enableSelect(); - - // Update location for status, upload, & new folder functions. - pgAdmin.FileUtils.setUploader(path); - if(user_input) { - $('.storage_dialog #uploader .input-path').val(path+user_input); - } - - // set default selected file type - if (file_type === '') { - file_type = $('.change_file_types select').val(); - } - - // navigate to directory or path when clicked on breadcrumbs - $('.file_manager a.breadcrumbs').off().on('click', function() { - var curr_path = $(this).attr('data-path'), - current_dir = $(this).html(), - move_to = curr_path.substring( - 0, curr_path.lastIndexOf(current_dir) - ) + current_dir; - - getFolderInfo(move_to); - enab_dis_level_up(); - }); - - // hide select file if we are listing drives in windows. - if (pgAdmin.FileUtils.hideButtons()) { - $('.allowed_file_types .change_file_types').hide(); - } else { - $('.allowed_file_types .change_file_types').show(); - } - - var loading_icon_url = url_for( - 'static', { - 'filename': 'img/loading.gif', - } - ); - - // Display an activity indicator. - $('.fileinfo').find('span.activity').html( - '' + gettext('Loading...') + '/>'
-    );
-
-    var post_data = {
-      'path': path,
-      'mode': 'getfolder',
-      'file_type': file_type || '*',
-      'show_hidden': $('#show_hidden').prop('checked'),
-    };
-
-    $.ajax({
-      type: 'POST',
-      data: JSON.stringify(post_data),
-      url: pgAdmin.FileUtils.fileConnector,
-      dataType: 'json',
-      contentType: 'application/json; charset=utf-8',
-      async: false,
-    })
-      .done(function(resp) {
-        $('.storage_dialog #uploader .input-path').prop('disabled', false);
-        var result = '',
-          data = resp.data.result;
-        let isGridView = false;
-        // hide activity indicator
-        $('.fileinfo').find('span.activity').hide();
-        if (data.Code === 0) {
-          Notify.error(data.Error);
-          return;
-        }
-
-        var $this, orig_value, newvalue;
-
-        // generate HTML for files/folder and render into container
-        if ($('.fileinfo').data('view') == 'grid') {
-          result += getGridView(data, capabilities);
-          isGridView = true;
-        } else {
-          result += getListView(data, capabilities);
-        }
-
-        // Add the new markup to the DOM.
-        $('.fileinfo .file_listing').html(result);
-
-        let $listing_table = $('.fileinfo .file_listing .file_listing_table');
-
-        $listing_table.tablesorter({
-          widgets: [ 'resizable', 'stickyHeaders' ],
-          widgetOptions : {
-            stickyHeaders_attachTo:'.file_listing',
-            stickyHeaders_offset: 0,
-            resizable_widths: ['400px', '100px', '175px'],
-          },
-        });
-
-        /* In order to fit our UI, some things need to be explicitly set
-         * as tablesorter resizable is creating trouble.
-         */
-        $listing_table.on( 'resizableComplete', function() {
-          let wo = this.config.widgetOptions;
-          $.tablesorter.resizable.setWidth($listing_table.find('th[data-column= 0) - $(this).click(); - // If folder then first select and then double click to open folder/drive - else if ($(this).find('.fa-folder-open').length || $(this).find('.fa-hdd').length) { - $(this).click(); - setTimeout(() => { $(self).trigger('dblclick'); }, 10); - } - }; - - $listing_table.on( 'tablesorter-ready', function() { - let wo = this.config.widgetOptions; - if($.tablesorter.storage($listing_table[0], 'tablesorter-table-resized-width') === '') { - $.tablesorter.resizable.setWidth($listing_table, $('.fileinfo .file_listing').width()); - } - $.tablesorter.resizable.setWidth($listing_table.find('th[data-column="2"]'), wo.resizable_widths[2]); - $listing_table.trigger('resizableUpdate'); - - // Table Sorter writes table elements randomly so we need to handle some corner cases manually - $('#show_hidden').off('keydown').on('keydown', function(event) { - if (!isGridView && event.keyCode == 9 && event.shiftKey) { - event.preventDefault(); - $listing_table.find('tbody tr:last').trigger('focus'); - } - }); - - $listing_table.find('tbody tr').off('keydown').on('keydown', function(event) { - // If key is pressed then we need to trigger click so that it can select file - if (event.keyCode == 13 || event.keyCode == 32) { - clickOnFileFolderManually.call(this, event); - } else if (event.keyCode == 9) { - if (event.shiftKey) { - // When first tr losses focus and shift + tab > we need to set focus on header - if ($(this).prev().length == 0) { - event.preventDefault(); - $listing_table.find('th.tablesorter-header:last').trigger('focus'); - } - } else { - // When last tr losses focus and Tab was pressed > we need to set focus on checkbox - if ($(this).next().length == 0) { - event.preventDefault(); - $('#show_hidden').trigger('focus'); - } - } - } - }); - - $listing_table.find('th.tablesorter-header').off('keydown').on('keydown', function(event) { - // If key is pressed then we need to trigger click so that it can sort - if (event.keyCode == 13 || event.keyCode == 32) { - event.preventDefault(); - event.stopPropagation(); - $(this).trigger('click'); - } - }); - }); - - if(isGridView) { - $('.file_manager').find('#contents li').off('keydown').on('keydown', function(event) { - // If key is pressed then we need to trigger click so that it can sort - if (event.keyCode == 13 || event.keyCode == 32) { - clickOnFileFolderManually.call(this, event); - } - }); - } - - // rename file/folder - $('.file_manager button.rename').off().on('click', function(e) { - - if ($('.fileinfo').data('view') == 'grid') { - e.stopPropagation(); - $this = $('.file_manager').find('#contents li.selected div'); - orig_value = decodeURI($this.find('span.less_text').attr('title')); - newvalue = orig_value.substring(0, orig_value.indexOf('.')); - - if (newvalue === '') { - newvalue = decodeURI(orig_value); - } - - $this.find('input').toggle().val(newvalue).trigger('focus'); - $this.find('span').toggle(); - - // Rename folder/file on pressing enter key - $('.file_manager').off().on('keyup', function(event) { - if (event.keyCode == 13) { - event.stopPropagation(); - $('.fileinfo #contents li.selected div').find( - 'input' - ).trigger('blur'); - } - }); - } else if ($('.fileinfo').data('view') == 'list') { - e.stopPropagation(); - $this = $('.fileinfo').find( - 'table#contents tbody tr.selected td.tbl_file' - ); - if($this.length == 0) { - $this = $('.fileinfo').find( - 'table#contents tbody tr.selected td.tbl_folder' - ); - // putting temporary class to distiguish between folder rename & double click. - $this.addClass('tbl_folder_rename'); - } - orig_value = decodeURI($this.find('span.less_text').html()); - newvalue = orig_value.substring(0, orig_value.lastIndexOf('.')); - - if (orig_value.lastIndexOf('/') == orig_value.length - 1 || (_.isEmpty(newvalue) || _.isUndefined(newvalue) || _.isNull(newvalue))) { - newvalue = decodeURI(orig_value); - } - - $this.find('.fm_file_rename').toggle().val(newvalue).trigger('focus'); - $this.find('.fm_file_name').toggle(); - - // Rename folder/file on pressing enter key - $('.file_manager').off().on('keyup', function(event) { - if (event.keyCode == 13) { - event.stopPropagation(); - $this.find('fm_file_rename').trigger('blur'); - } - }); - } - }); - - // Rename UI handling - $('.fileinfo #contents li div').on('blur dblclick', 'input', function(e) { - e.stopPropagation(); - - var old_name = decodeURI($(this).siblings('div').find('.less_text').attr('title')); - newvalue = old_name.substring(0, old_name.indexOf('.')); - var last = getFileExtension(old_name), - file_data, new_name, file_path, full_name; - - if (old_name.indexOf('.') == 0) { - last = ''; - } - - if (newvalue == '') { - newvalue = decodeURI(old_name); - } - - if (e.type == 'keydown') { - if (e.which == 13) { - full_name = decodeURI($(this).val()) + ( - last !== '' ? '.' + last : '' - ); - - $(this).toggle(); - $(this).siblings('span').toggle().html(full_name); - - new_name = decodeURI($(this).val()); - file_path = decodeURI($(this).parent().parent().find( - 'span' - ).attr('data-alt')); - file_data = { - 'Filename': old_name, - 'Path': file_path, - 'NewFilename': new_name, - }; - - if (newvalue !== new_name) { - renameItem(file_data); - var parent = $('.currentpath').val(); - getFolderInfo(parent); - } - e.stopPropagation(); - } - - if ( - e.which == 38 || e.which == 40 || e.which == 37 || - e.which == 39 || e.keyCode == 32 - ) { - e.stopPropagation(); - } - } else if (e.type == 'focusout') { - if ($(this).css('display') == 'inline-block' || $(this).css('display') == 'inline') { - full_name = decodeURI( - $(this).val() - ) + (last !== '' ? '.' + last : ''); - - if (newvalue !== new_name) { - - $(this).toggle(); - $(this).siblings('span').toggle().html(full_name); - - //check if user is trying to rename folder - let isFolder = $(this).closest('.tbl_folder').length > 0; - - new_name = decodeURI($(this).val()); - file_path = decodeURI($(this).parent().parent().find( - 'span' - ).attr('data-alt')); - file_data = { - 'Filename': old_name, - 'Path': file_path, - 'NewFilename': new_name, - 'isFolder': isFolder, - }; - - renameItem(file_data); - let current_path = $('.currentpath').val(); - if(isFolder == true) { - // if its folder rename, remove the temporary added class - $(this).closest('.tbl_folder').removeClass('tbl_folder_rename'); - if(current_path.includes('\\')) { - current_path = $('.currentpath').val().split('\\').slice(0, -2).join('\\')+'\\'; - } - else { - current_path = $('.currentpath').val().split('/').slice(0, -2).join('/')+'/'; - } - } - getFolderInfo(current_path); - } - } - } else { - e.stopPropagation(); - } - }); - - $('.fileinfo table#contents tr td div').on( - 'blur dblclick', 'input', - function(e) { - var old_name = decodeURI($(this).siblings('div').find('.less_text').attr('title')), - new_value = old_name.substring(0, old_name.indexOf('.')), - last = getFileExtension(old_name); - if (old_name.indexOf('.') == 0) { - last = ''; - } - - if (new_value == '') { - new_value = old_name; - } - - if (e.type == 'focusout') { - if ($(this).css('display') == 'inline-block' || $(this).css('display') == 'inline') { - var full_name = decodeURI($(this).val()) + ( - last !== '' ? '.' + last : '' - ); - - if (new_value !== new_name) { - - $(this).toggle(); - $(this).siblings('span').toggle().html(full_name); - - let isFolder = $(this).closest('.tbl_folder').length > 0; - - var new_name = decodeURI($(this).val()), - file_path = decodeURI($(this).parent().parent().attr('title')), - file_data = { - 'Filename': old_name, - 'Path': file_path, - 'NewFilename': new_name, - 'isFolder': isFolder, - }; - - renameItem(file_data); - let current_path = $('.currentpath').val(); - if(isFolder == true) { - // if its folder rename, remove the temporary added class - $(this).closest('.tbl_folder').removeClass('tbl_folder_rename'); - if(current_path.includes('\\')) { - current_path = $('.currentpath').val().split('\\').slice(0, -2).join('\\')+'\\'; - } - else { - current_path = $('.currentpath').val().split('/').slice(0, -2).join('/')+'/'; - } - } - getFolderInfo(current_path); - } - } - } else { - e.stopPropagation(); - } - }); - - var data_cap = {}; - data_cap.Capabilities = capabilities; - /* - * Bind click events - * Select items - afolder dblclick - */ - if ($('.fileinfo').data('view') == 'grid') { - // Get into folder on dblclick - $('.fileinfo').find('#contents li').dblclick(function(e) { - e.stopPropagation(); - // Enable/Disable level up button - enab_dis_level_up(); - - var file_path = decodeURI($(this).find('span').attr('data-alt')); - - if (file_path.lastIndexOf('/') == file_path.length - 1 || file_path.lastIndexOf('\\') == file_path.length - 1) { - $('.file_manager_ok').addClass('disabled'); - $('.file_manager_ok').attr('disabled', true); - $('.file_manager button.delete, .file_manager button.rename').attr('disabled', 'disabled'); - $('.file_manager button.download').attr('disabled', 'disabled'); - - getFolderInfo(file_path); - - } else { - var is_valid_file = getFileInfo(file_path); - if (is_valid_file && check_file_capability(e, data_cap, 'grid')) { - $('.file_manager_ok').trigger('click'); - } - } - }); - - $('.fileinfo').find('#contents li').on('click', function(e) { - e.stopPropagation(); - var file_path = decodeURI($(this).find('.clip span').attr('data-alt')), - is_protected = $(this).find( - '.clip span.fm_lock_icon' - ).attr('data-protected'); - - if (file_path.lastIndexOf('/') == file_path.length - 1 || file_path.lastIndexOf('\\') == file_path.length - 1) { - if ( - has_capability(data_cap, 'select_folder') && - is_protected == undefined - ) { - $(this).parent().find('li.selected').removeClass('selected'); - $(this).addClass('selected'); - - $('.file_manager_ok').removeClass('disabled'); - $('.file_manager_ok').attr('disabled', false); - $('.file_manager button.delete, .file_manager button.rename').removeAttr( - 'disabled', 'disabled' - ); - $('.file_manager button.download').attr( - 'disabled', 'disabled' - ); - // set selected folder name in breadcrums - $('.file_manager #uploader .input-path').hide(); - $('.file_manager #uploader .show_selected_file').remove(); - $('' + file_path + '').appendTo( - '.file_manager #uploader .filemanager-path-group' - ); - } - pgAdmin.FileUtils.setUploader(file_path); - } else { - if ( - has_capability(data_cap, 'select_file') && - is_protected == undefined - ) { - $(this).parent().find('li.selected').removeClass('selected'); - $(this).addClass('selected'); - $('.file_manager_ok').removeClass('disabled'); - $('.file_manager_ok').attr('disabled', false); - $('.file_manager button.delete, .file_manager button.download, .file_manager button.rename').removeAttr( - 'disabled' - ); - // set selected folder name in breadcrums - $('.file_manager #uploader .show_selected_file').remove(); - } - - getFileInfo(file_path); - } - }); - } else { - $('.fileinfo table#contents tbody tr').on('click', function(e) { - e.stopPropagation(); - var file_path = decodeURI($('td:first-child', this).attr('title')), - is_protected = $('td:first-child', this).find( - 'i.tbl_lock_icon' - ).attr('data-protected'); - - if (file_path.lastIndexOf('/') == file_path.length - 1 || file_path.lastIndexOf('\\') == file_path.length - 1) { - if (has_capability(data_cap, 'select_folder') && is_protected == undefined) { - $(this).parent().find('tr.selected').removeClass('selected'); - $('td:first-child', this).parent().addClass('selected'); - $('.file_manager_ok').removeClass('disabled'); - $('.file_manager_ok').attr('disabled', false); - $('.file_manager button.download').attr('disabled', 'disabled'); - $('.file_manager button.delete, .file_manager button.rename').removeAttr('disabled'); - - // set selected folder name in breadcrums - $('.file_manager #uploader .input-path').hide(); - $('.file_manager #uploader .show_selected_file').remove(); - $('' + file_path + '').appendTo( - '.file_manager #uploader .filemanager-path-group' - ); - } - pgAdmin.FileUtils.setUploader(file_path); - } else { - if (has_capability(data_cap, 'select_file') && is_protected == undefined) { - $(this).parent().find('tr.selected').removeClass('selected'); - $('td:first-child', this).parent().addClass('selected'); - $('.file_manager button.delete, .file_manager button.download, .file_manager button.rename').removeAttr( - 'disabled' - ); - // set selected folder name in breadcrums - $('.file_manager #uploader .show_selected_file').remove(); - } - - getFileInfo(file_path); - } - }); - - $('.fileinfo table#contents tbody tr').on('dblclick', function(e) { - e.stopPropagation(); - // Enable/Disable level up button - enab_dis_level_up(); - var file_path = $('td:first-child', this).attr('title'); - - if (file_path.lastIndexOf('/') == file_path.length - 1 || file_path.lastIndexOf('\\') == file_path.length - 1) { - $('.file_manager_ok').addClass('disabled'); - $('.file_manager_ok').attr('disabled', true); - $('.file_manager button.download').attr('disabled', 'disabled'); - $('.file_manager button.delete, .file_manager button.rename').attr('disabled', 'disabled'); - getFolderInfo(file_path); - } else { - var is_valid_file = getFileInfo(file_path); - if ( - is_valid_file && check_file_capability(e, data_cap, 'table') - ) { - $('.file_manager_ok').trigger('click'); - } - } - }); - - } - }) - .fail(function() { - $('.storage_dialog #uploader .input-path').prop('disabled', false); - }); - }; - var homedir='/'; - // Enable/Disable level up button - var enab_dis_level_up = function() { - $('.file_manager #uploader .input-path').show(); - $('.show_selected_file').remove(); - - setTimeout(function() { - var b = $('.currentpath').val(), - $level_up = $('.file_manager').find('button.level-up'), - $home_btn = $('.file_manager').find('button.home'); - - (b === '/') ? $level_up.attr('disabled', 'disabled') : $level_up.removeAttr('disabled'); - (b === homedir) ? $home_btn.attr('disabled', 'disabled') : $home_btn.removeAttr('disabled'); - - }, 100); - }; - - // Check if user can Select file - var check_file_capability = function(event, data_cap, view_type) { - var current_element = event.currentTarget, - is_protected; - - if (view_type == 'grid') { - is_protected = $(current_element).find( - '.clip span.fm_lock_icon' - ).attr('data-protected'); - } else { - is_protected = $(current_element).find('td:first-child').find( - 'i.tbl_lock_icon' - ).attr('data-protected'); - } - - return has_capability(data_cap, 'select_file') && - is_protected == undefined; - }; - - // Download selected file - var download_file = function (path) { - - var data = { 'path': path, 'mode': 'download' }, - params = {}; - - params[pgAdmin.csrf_token_header] = pgAdmin.csrf_token; - - $.ajax({ - type: 'POST', - url: pgAdmin.FileUtils.fileConnector, - contentType: false, - headers: params, - xhrFields: { - responseType: 'blob', - }, - cache: false, - data: JSON.stringify(data), - success: function (blob, status, xhr) { - // check for a filename - var filename = xhr.getResponseHeader('filename'); - - if (typeof window.navigator.msSaveBlob !== 'undefined') { - // IE workaround for "HTML7007: One or more blob URLs were revoked by closing the blob for which they were created. These URLs will no longer resolve as the data backing the URL has been freed." - window.navigator.msSaveBlob(blob, filename); - } else { - var URL = window.URL || window.webkitURL; - var downloadUrl = URL.createObjectURL(blob); - - if (filename) { - // use HTML5 a[download] attribute to specify filename - var a = document.createElement('a'); - // safari doesn't support this yet - if (typeof a.download === 'undefined') { - window.location.href = downloadUrl; - } else { - a.href = downloadUrl; - a.download = filename; - document.body.appendChild(a); - a.click(); - } - } else { - window.location.href = downloadUrl; - } - - setTimeout(function () { URL.revokeObjectURL(downloadUrl); }, 100); // cleanup - } - }, - error: function (error) { - Notify.error(error); - }, - }); - }; - - /*--------------------------------------------------------- - Initialization - Entry point - ---------------------------------------------------------*/ - /* - * get transaction id to generate request url and - * to generate config files on runtime - */ - pgAdmin.FileUtils = { - init: function() { - var fm_url = url_for('file_manager.get_trans_id'), - transId = loadData(fm_url), - t_res, - t_id; - - if (transId.readyState == 4) { - t_res = JSON.parse(transId.responseText); - } - t_id = _.isUndefined(t_res) ? 0 : t_res.data.fileTransId; - var root_url = url_for('file_manager.index'), - file_manager_config_json = root_url + t_id + '/file_manager_config.json', - fileConnector = root_url + 'filemanager/' + t_id + '/', - cfg = loadData(file_manager_config_json), - config; - - this.fileConnector = fileConnector; - this.transId = t_id; - // load user configuration file - if (cfg.readyState == 4) { - this.config = config = JSON.parse(cfg.responseText); - homedir=config.options.homedir; - } - - if (_.isUndefined(config)) - return; - - // set main url to filemanager and its capabilites - var fileRoot = config.options.fileRoot, - capabilities = config.options.capabilities; - - /* - * Get localized messages from file - * through culture var or from URL - */ - - var lg = [], - enjson = url_for('file_manager.index') + 'en.json', - lgf = loadData(enjson); - - if (lgf.readyState == 4) { - this.lg = lg = JSON.parse(lgf.responseText); - } - - // create and enable user to create new file - if ( - config.options.dialog_type == 'select_file' || - config.options.dialog_type == 'create_file' || - config.options.dialog_type == 'storage_dialog' - ) { - // Create file selection dropdown - var allowed_types = config.options.allowed_file_types, - types_len = allowed_types.length, - select_box = ''; - - if (types_len > 0) { - var i = 0, - t, - have_all_types = false; - - let fileFormats = ''; - let response = getFileFormat(config.options.allowed_file_types); - let lastSelectedFormat = response.responseJSON.info; - - while (i < types_len) { - t = allowed_types[i]; - if ((types_len == 1 || t != '*')) { - if(t === lastSelectedFormat) - fileFormats += ''; - else - fileFormats += ''; - have_all_types = (have_all_types || (t == '*')); - } else if ((lastSelectedFormat === '*')) { - fileFormats += ''; - have_all_types = (have_all_types || (t == '*')); - } - i++; - } - - if (!have_all_types) { - fileFormats += ''; - } - - select_box = `
    -
    ` + - gettext('Show hidden files and folders?') + - ` -
    -
    - - -
    `; - } - - $('.allowed_file_types').html(select_box); - - $('.allowed_file_types select').on('change', function() { - var selected_val = $(this).val(), - curr_path = $('.currentpath').val(), - user_input_file = null, - input_path = $('.storage_dialog #uploader .input-path').val(); - config.options.selectedFormat = selected_val; - $.ajax({ - url: url_for('settings.save_file_format_setting'), - type: 'POST', - contentType: 'application/json', - data: JSON.stringify(config.options), - }); - if (curr_path.endsWith('/')) { - user_input_file = input_path.substring(curr_path.lastIndexOf('/')+1); - } else { - user_input_file = input_path.substring(curr_path.lastIndexOf('\\')+1); - } - getFolderInfo(curr_path, selected_val, user_input_file); - }); - - // If user have preference to show hidden files - if (config.options.show_hidden_files) { - setTimeout(function() { - $('#show_hidden').trigger('click'); - }, 10); - } - // handle show hidden files functionality - this.handleClick = function(cb) { - var tmp_data = { - 'is_checked': false, - }; - - if (cb.checked) { - $('div.allowed_file_types select').trigger('change'); - tmp_data['is_checked'] = true; - } else { - // User wants to hide it again - $('div.allowed_file_types select').trigger('change'); - tmp_data['is_checked'] = false; - } - // Save it in preference - save_show_hidden_file_option(tmp_data['is_checked'], pgAdmin.FileUtils.transId); - }; - } - - /*--------------------------------------------------------- - Item Actions - Object events - ---------------------------------------------------------*/ - - $('.file_manager').attr('data-platform', config.options.platform_type); - - // Switch to folder view - $('.file_manager .fileinfo').on('click', function() { - enable_disable_btn(); - }); - - - - // Refresh current directory - $('.file_manager .refresh').on('click', function() { - enable_disable_btn(); - - var curr_path = $('.currentpath').val(), - path; - - $('.file_manager #uploader .input-path').val(curr_path); - if (curr_path.endsWith('/')) { - path = curr_path.substring(0, curr_path.lastIndexOf('/')) + '/'; - } else { - path = curr_path.substring(0, curr_path.lastIndexOf('\\')) + '\\'; - } - getFolderInfo(path); - }); - - // hide message prompt and dimmer if clicked no - $('.delete_item button.btn_no').on('click', function() { - $('.delete_item, .fileinfo .fm_dimmer').hide(); - }); - - // Disable button on load - $('.file_manager').find('button.rename').attr('disabled', 'disabled'); - - // stop click event on dimmer click - $('.fileinfo .fm_dimmer').on('click', function(e) { - e.stopPropagation(); - }); - - $('.fileinfo .replace_file').not( - $(this).find('span.pull-right') - ).on( - 'click', - function(e) { - $('#uploader .filemanager-btn-group').off().on( - 'click', - function() { - $('.fileinfo .delete_item, .fileinfo .replace_file, .fileinfo .fm_dimmer').hide(); - }); - e.stopPropagation(); - }); - - // Set initial view state. - $('.fileinfo').data('view', config.options.defaultViewMode); - setViewButtonsFor(config.options.defaultViewMode); - - // Upload click event - $('.file_manager .uploader').on('click', 'a', function(e) { - e.preventDefault(); - var b = $('.currentpath').val(), - node_val = $(this).next().text(), - parent = b.substring(0, b.slice(0, -1).lastIndexOf(node_val)); - getFolderInfo(parent); - }); - - // re-render the home view - $('.file_manager .home').on('click', function() { - var currentViewMode = $('.fileinfo').data('view'); - $('.fileinfo').data('view', currentViewMode); - getFolderInfo(homedir); - enab_dis_level_up(); - }); - - // Go one directory back - $('.file_manager .level-up').on('click', function() { - var b = $('.currentpath').val(); - // Enable/Disable level up button - enab_dis_level_up(); - - if (b.endsWith('\\') || b.endsWith('/')) { - b = b.substring(0, b.length - 1); - } - - if (b != '/') { - var parent; - if (b.lastIndexOf('/') > b.lastIndexOf('\\')) { - parent = b.substring(0, b.slice(0, -1).lastIndexOf('/')) + '/'; - } else { - parent = b.substring(0, b.slice(0, -1).lastIndexOf('\\')) + '\\'; - } - - var d = $('.fileinfo').data('view'); - $('.fileinfo').data('view', d); - getFolderInfo(parent); - } - }); - - // set buttons to switch between grid and list views. - $('.file_manager .grid').on('click',() => { - setViewButtonsFor('grid'); - $('.fileinfo').data('view', 'grid'); - enable_disable_btn(); - getFolderInfo($('.currentpath').val()); - save_file_dialog_view('grid', pgAdmin.FileUtils.transId); - }); - - // Show list mode - $('.file_manager .list').on('click',() => { - setViewButtonsFor('list'); - $('.fileinfo').data('view', 'list'); - enable_disable_btn(); - getFolderInfo($('.currentpath').val()); - save_file_dialog_view('list', pgAdmin.FileUtils.transId); - }); - - // Provide initial values for upload form, status, etc. - pgAdmin.FileUtils.setUploader(fileRoot); - - var data; - this.data = data = { - 'Capabilities': capabilities, - }; - - function InputObject() { - this.init = function(cap) { - var self = this, - check_obj = function(path, check) { - - path = decodeURI(path); - if (path.lastIndexOf('/') == path.length - 1 || path.lastIndexOf('\\') == path.length - 1) { - if ( - has_capability(self.data_cap, 'select_folder') - ) { - $('.file_manager_ok').removeClass('disabled'); - $('.file_manager_ok').attr('disabled', false); - $('.file_manager button.delete').removeAttr( - 'disabled', 'disabled' - ); - $('.file_manager button.download').attr( - 'disabled', 'disabled' - ); - $('.file_manager button.rename').attr('disabled', 'disabled'); - // set selected folder name in breadcrums - $('.file_manager #uploader .show_selected_file').remove(); - $('' + path + '').appendTo( - '.file_manager #uploader .filemanager-path-group' - ); - } else { - $('.file_manager_ok').addClass('disabled'); - $('.file_manager_ok').attr('disabled', true); - if (check) { - // Enable/Disable level up button - enab_dis_level_up(); - - $('.file_manager button.delete, .file_manager button.rename').attr('disabled', 'disabled'); - $('.file_manager button.download').attr('disabled', 'disabled'); - getFolderInfo(path); - } - } - } else { - if (has_capability(self.data_cap, 'select_folder')) { - disableSelect(); - } - - if ( - has_capability(self.data_cap, 'select_file') - ) { - $('.file_manager_ok').removeClass('disabled'); - $('.file_manager_ok').attr('disabled', false); - $('.file_manager button.delete, .file_manager button.download, .file_manager button.rename').removeAttr( - 'disabled' - ); - // set selected folder name in breadcrumbs - $('.file_manager #uploader .show_selected_file').remove(); - } - - if (check) { - if (config.options.dialog_type == 'create_file') { - var status = checkPermission(path); - if (status) { - $('.file_manager_ok').trigger('click'); - } - } else if (config.options.dialog_type == 'select_file') { - var file_status = getFileInfo(path); - if (file_status) { - $('.file_manager_ok').trigger('click'); - } - } - } - } - }; - - self.data_cap = cap; - - $('.storage_dialog #uploader .input-path').on('keyup',function(e) { - if (e.keyCode == 13) { - e.stopPropagation(); - var path = $(this).val(); - if (path == '') { - path = '/'; - } - - if (config.options.platform_type === 'win32') { - path = path.replace(/\//g, '\\'); - } else { - path = path.replace(/\\/g, '/'); - if (!path.startsWith('/')) { - path = '/' + path; - } - } - - $(this).val(path); - setTimeout(function() { - check_obj(path, true); - }); - - return; - } - check_obj($(this).val(), false); - }); - }; - this.set_cap = function(cap) { - this.data_cap = cap; - }; - } - var input_object = new InputObject(); - input_object.init(data); - - // Upload file - if (has_capability(data, 'upload')) { - Dropzone.autoDiscover = false; - // we remove simple file upload element - $('.file-input-container').remove(); - $('.upload').remove(); - $('.create').before(' '); - - $('#uploader .upload').off().on('click', function() { - // we create prompt - var msg = '
    ' + - '' + - '
    '+ - '
    '+ - '
    ' + - '
    ' + gettext('Drop files here to upload.') + ' ' + lg.file_size_limit + - config.upload.fileSizeLimit + ' ' + lg.mb + '.
    ', - path = $('.currentpath').val(), - filesizelimit = config.upload.fileSizeLimit, - // default dropzone value - fileSize = (filesizelimit != 'auto') ? filesizelimit : 256, - acceptFiles; - - if (config.security.uploadPolicy == 'DISALLOW_ALL') { - acceptFiles = '.' + config.security.uploadRestrictions.join(',.'); - } else { - // We allow any extension since we have no easy way to handle the the - // built-in `acceptedFiles` params would be handled later by the - // connector. - acceptFiles = null; - } - - $('.file_manager .upload_file').toggleClass('d-none'); - $('.file_manager .file_listing').toggleClass('d-none'); - $('.file_manager .upload_file').html(msg); - - var previewTemplate = '
    ' + - '
    ' + - '

    ' + - '

    ' + - '
    ' + - '
    ' + - '
    ' + - '
    ' + - '
    ' + - '' + - '
    '; - - // We need to append our csrf token with dropzone's ajax request header - let csrfToken = {}; - csrfToken[pgAdmin.csrf_token_header] = pgAdmin.csrf_token; - - $('div#multiple-uploads').dropzone({ - paramName: 'newfile', - url: pgAdmin.FileUtils.fileConnector, - headers: csrfToken, - maxFilesize: fileSize, - maxFiles: config.upload.number, - addRemoveLinks: true, - previewTemplate: previewTemplate, - parallelUploads: config.upload.number, - dictMaxFilesExceeded: lg.dz_dictMaxFilesExceeded.replace( - '%s', config.upload.number - ), - dictDefaultMessage: lg.dz_dictDefaultMessage, - dictInvalidFileType: lg.dz_dictInvalidFileType, - dictFileTooBig: lg.file_too_big + ' ' + lg.file_size_limit + - config.upload.fileSizeLimit + ' ' + lg.mb, - acceptedFiles: acceptFiles, - autoProcessQueue: true, - init: function() { - $('.dz_cross_btn').off().on('click', function() { - $('.file_manager .upload_file').toggleClass('d-none'); - $('.file_manager .file_listing').toggleClass('d-none'); - }); - }, - sending: function(file, xhr, formData) { - formData.append('mode', 'add'); - formData.append('currentpath', path); - $('.upload_file .dz_cross_btn').attr('disabled', 'disabled'); - setTimeout(function() {/*This is intentional (SonarQube)*/}, 10000); - }, - success: function(file, response) { - var resp_data = response.data.result, - $this = $(file.previewTemplate); - - if (resp_data.Code == 1) { - setTimeout(function() { - $this.find('.dz-upload').addClass('success'); - }, 1000); - $this.find('.dz-upload').css('width', '100%').html('100%'); - Notify.success(lg.upload_success); - } else { - $this.find('.dz-upload').addClass('error'); - $this.find('.dz-upload').css('width', '0%').html('0%'); - Notify.error(resp_data.Error); - } - getFolderInfo(path); - }, - totaluploadprogress: function() {/*This is intentional (SonarQube)*/}, - complete: function(file) { - if (file.status == 'error') { - Notify.error(lg.upload_error); - } - $('.upload_file .dz_cross_btn').removeAttr('disabled'); - getFolderInfo(path); - }, - }); - }); - } - - this.getDetailView(fileRoot); - }, - /* - * Sets the folder status, upload, and new folder functions - * to the path specified. Called on initial page load and - * whenever a new directory is selected. - */ - setUploader: function(path) { - var config = this.config; - var lg = this.lg; - $('.storage_dialog #uploader').find('a').remove(); - $('.storage_dialog #uploader').find('b').remove(); - - if (config.options.platform_type === 'win32') { - path = path.replace(/\//g, '\\'); - } else { - path = path.replace(/\\/g, '/'); - } - - path = decodeURI(path); - if (config.options.platform_type === 'win32') { - if (config.options.show_volumes && path == '\\') { - $('.storage_dialog #uploader .input-path').val(''); - } else { - $('.storage_dialog #uploader .input-path').val(path); - } - } else if ((config.options.platform_type !== 'win32') && - (path == '' || !path.startsWith('/'))) { - path = '/' + path; - $('.storage_dialog #uploader .input-path').val(path); - } else { - $('.storage_dialog #uploader .input-path').val(path); - } - - if (path.lastIndexOf('\\') == -1 && path.lastIndexOf('/') == -1) { - $('.currentpath').val(path); - } else if (path.lastIndexOf('/') > path.lastIndexOf('\\')) { - $('.currentpath').val(path.substr(0, path.lastIndexOf('/') + 1)); - } else { - $('.currentpath').val(path.substr(0, path.lastIndexOf('\\') + 1)); - } - - enab_dis_level_up(); - if ($('.storage_dialog #uploader h1 span').length === 0) { - $('' + lg.current_folder + '').appendTo($('.storage_dialog #uploader h1')); - } - - $('.storage_dialog #uploader .input-path').attr('title', path); - $('.storage_dialog #uploader .input-path').attr('data-path', path); - enableSelect(); - - // create new folder - $('.create').off().on('click', function() { - var foldername = lg.new_folder; - var $file_element, - $file_element_list, - folder_div; - - - $('.file_manager button.create').attr('disabled', 'disabled'); - $('.no_folder_found').addClass('d-none'); - if ($('.fileinfo').data('view') == 'grid') { - - // template for creating new folder - folder_div = - '
  • ' + - '
    ' + - '
    New_Folder
    ' + - '
  • '; - - path = $('.currentpath').val(); - $file_element = $(folder_div); - $('.fileinfo #contents.grid').prepend($file_element); - $file_element.find('div span.less_text').toggle(); - $file_element.find('div input').toggle().val(lg.new_folder).select(); - - // rename folder/file on pressing enter key - $('.file_manager').on('keyup', function(e) { - if (e.keyCode == 13) { - e.stopPropagation(); - $file_element.find('div input').trigger('blur'); - } - }); - - // rename folder/file on blur - $file_element.find('div input').on('blur', function() { - $('.file_manager button.create').removeAttr('disabled'); - var text_value = $file_element.find('div input').val(); - - path = $('.currentpath').val(); - - $file_element.find('div input').toggle(); - $file_element.find('div span.less_text').toggle().html(text_value); - if (text_value === undefined) { - text_value = lg.new_folder; - } - getFolderName(text_value); - getFolderInfo(path); - }); - - } else if ($('.fileinfo').data('view') == 'list') { - // template to create new folder in table view - folder_div = $( - ` - -
    - -
    -
    - - ${lg.new_folder} -
    -
    -
    - - - - ` - ); - - $file_element_list = $(folder_div); - let tableEl = $('.fileinfo #contents.file_listing_table'); - tableEl.removeClass('file_listing_table_no_data'); - tableEl.find('tbody').prepend($file_element_list); - - $file_element_list.find('td .fm_file_name').toggle(); - $file_element_list.find('td input').toggle().val(lg.new_folder).select(); - - // rename folder/file on pressing enter key - $('.file_manager').on('keyup', function(e) { - if (e.keyCode == 13) { - e.stopPropagation(); - $file_element_list.find('td input').trigger('blur'); - } - }); - - // rename folder/file on blur - $file_element_list.find('td input').on('blur', function() { - $('.file_manager button.create').removeAttr('disabled'); - var text_value = $file_element_list.find('td input').val(); - path = $('.currentpath').val(); - $file_element_list.find('td input').toggle(); - $file_element_list.find('td .fm_file_name span.less_text').html(text_value); - $file_element_list.find('td .fm_file_name').toggle(); - if (text_value === undefined) { - text_value = lg.new_folder; - } - getFolderName(text_value); - getFolderInfo(path); - }); - } - - // create a new folder - var getFolderName = function(value) { - var fname = value; - - if (fname != '') { - foldername = fname; - // Add _ variable in URL for avoiding the caching - $.getJSON( - pgAdmin.FileUtils.fileConnector + '?_=' + Date.now() + '&mode=addfolder&path=' + $('.currentpath').val() + '&name=' + foldername, - function(resp) { - var result = resp.data.result; - if (result.Code === 1) { - Notify.success(lg.successful_added_folder); - getFolderInfo(result.Parent); - } else { - Notify.error(result.Error); - } - } - ); - } else { - Notify.error(lg.no_foldername); - } - }; - - }); - }, - /* Decides whether to retrieve file or folder info based on - * the path provided. - */ - getDetailView: function(path) { - if (path.lastIndexOf('/') == path.length - 1 || path.lastIndexOf('\\') == path.length - 1) { - var allowed_types = this.config.options.allowed_file_types; - - let set_type; - - let response = getFileFormat(this.config.options.allowed_file_types); - let lastSelectedFormat = response.responseJSON.info; - if (_.isUndefined(lastSelectedFormat) || allowed_types.indexOf(lastSelectedFormat) < 0) - set_type = allowed_types[0]; - else - set_type = lastSelectedFormat; - getFolderInfo(path, set_type); - } - }, - // helpful in show/hide toolbar button for Windows - hideButtons: function() { - return ( - this.config.options.platform_type === 'win32' && - $('.currentpath').val() === '' - ); - }, - - }; - return pgAdmin.FileUtils; -}); -//@ sourceURL=utility.js diff --git a/web/pgadmin/misc/file_manager/static/scss/_file_manager.scss b/web/pgadmin/misc/file_manager/static/scss/_file_manager.scss deleted file mode 100644 index 4d15c348c..000000000 --- a/web/pgadmin/misc/file_manager/static/scss/_file_manager.scss +++ /dev/null @@ -1,366 +0,0 @@ -#uploader h1 b { - font-weight: normal; - color: $color-gray; -} - -.file_listing { - min-width: 100%; - position: relative; - overflow: auto; - - .file_listing_table_no_data { - height: auto !important; - } - - .file_listing_table { - table-layout: fixed; - & td, &th { - text-overflow: ellipsis; - white-space: nowrap; - } - } - - .file_listing_table thead tr { - border-bottom: $panel-border; - } - - .file_listing_table tbody tr { - max-width: 100%; - width: 100%; - } - - .file_listing_table tbody tr td:nth-child(1), - .file_listing_table thead tr th:nth-child(1) { - width: 400px; - min-width: 100px; - } - - .file_listing_table tbody tr td:nth-child(2), - .file_listing_table thead tr th:nth-child(2) { - width: 100px; - min-width: 100px; - } - - .file_listing_table tbody tr td:nth-child(3), - .file_listing_table thead tr th:nth-child(3) { - width: 200px; - min-width: 200px; - max-width: 200px; - } -} - -.file_listing #contents.grid li:hover, -.file_listing #contents.grid li.selected { - cursor: pointer; - border: $table-hover-border; - background: $grid-hover-bg-color; - color: $grid-hover-fg-color; -} - -.fileinfo #contents li span.less_text { - width: 100%; - text-overflow: ellipsis; - overflow: hidden; - text-align: center; - white-space: nowrap; - display: block; -} - -.fileinfo table#contents tr td { - font-family: $font-family-primary; - - & span.less_text { - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; - display: block; - } - - & .fa { - line-height: inherit; - } -} - -.fm_folder_grid, .fm_file_grid { - font-size: xx-large !important; -} - -.fm_folder_list, -.fm_folder_grid, -.fm_file_grid, -.fm_file_list { - color: $color-fg; -} - -.fm_drive { - font-size: xx-large !important; - color: $color-gray; -} - -.newfile { - position: absolute; - top:0; - left: 3px; - right:0; - width: 152px; - height:23px; - opacity:0; filter: alpha(opacity=0); - cursor: pointer; - border:1px solid $color-primary; -} - -.file_listing #contents.grid li { - display: block; - float: left; - width: 100px; - height: 80px; - text-align: center; - overflow: hidden; - margin: 0.5rem; - -webkit-border-radius: 2px; - -moz-border-radius: 2px; - border-radius: 2px; - border: 1px solid $color-bg; -} - -.file_manager { - position: absolute; - top: 0px; - bottom: 0px; - left: 0px; - right: 0px; -} - -.file_manager #uploader { - border-bottom: $panel-border; -} - -.file_manager #uploader .filemanager-path-group { - padding: 0; - display: block; - border: 1px solid $border-color; - height: 30px; - border-radius: 5px; - -webkit-border-radius: 5px; - -moz-border-radius: 5px; - float: left; - margin-right: 10px; - background: $color-gray-lighter; -} - -.file_manager #uploader .btn-group .btn[disabled] { - color: $color-gray-light; - background-color: $color-gray-lighter; -} - -.file_manager #uploader .filemanager-btn-group { - border: 1px solid $border-color; - border-radius: 5px; - -webkit-border-radius: 5px; - -moz-border-radius: 5px; - width: auto; - float: left; - overflow: hidden; -} - -.file_manager .upload_file #dropzone-container { - background: $color-gray-light; -} - -.fileinfo .prompt-info { - text-align: center; - color: $color-fg; -} - -.allowed_file_types { - border-top: $panel-border; - background: $color-bg; - z-index: 5; - padding: 0.25rem; -} - -.upload_file{ - min-width: 100%; -} - -.upload_file .file_upload_main { - height: 127px; - width: 120px; - display: inline-block; - margin: 0 15px 15px 0 !important; - border: 1px solid $color-bg; - position: relative; - border-radius: 5px; - -webkit-border-radius: 5px; - -moz-border-radius: 5px; - background: $color-bg; - opacity: 1; -} - -.upload_file .file_upload_main .show_error { - padding: 10px 0 0 10px; - color: $color-fg; -} - -.file_upload_main .dz-progress { - float: left; - width: 100%; - height: 21px !important; - border: 1px solid $border-color; - border-radius: 0 !important; - -moz-border-radius: 0 !important; - -webkit-border-radius: 0 !important; -} - -.file_upload_main .dz-progress .dz-upload { - background: $color-primary-light !important; - text-align: center; -} - -.file_upload_main .dz-progress .dz-upload.success { - background: $color-success-light !important; - float: left; - width: 100%; -} - -.upload_file .file_upload_main a.dz_file_remove { - position: absolute; - top: 0; - right: 0; - color: $color-danger; - cursor: pointer; - border-radius: 5px; - -moz-border-radius: 5px; - -webkit-border-radius: 5px; - font-size: large; -} - -.upload_file .file_upload_main a.dz_file_remove:hover { - border: 1px solid $color-fg; -} - -.dropzone .dz-message { - color: $color-gray; -} - -.fileinfo .fm_dimmer { - display: none; - top: 0; - bottom: 0; - background: $loading-bg; - opacity: 0.5; - width: 100%; - position: absolute; - z-index: 3; -} - -.fileinfo .delete_item, .fileinfo .replace_file { - display: none; - padding: 1rem; - border-bottom: $panel-border; - background: $color-bg; - position: absolute; - top: 0; - left: 0; - right: 0; - z-index: 4; -} - -.upload_file .dz_cross_btn { - color: $color-fg; - right: 0px; - position: absolute; - background: transparent; - border: none; -} - -.file_manager .fileinfo #contents .fm_lock_icon { - color: $color-danger; - position: absolute; - top: 6px; - right: 0; - left: 19px; - font-size: 16px; -} - -.file_manager .fileinfo #contents .fa-lock.tbl_lock_icon { - color: $color-danger; - position: relative; - left: -5px; - top: -5px; - font-size: 10px; -} - -.file_manager button.ON { - background: $color-primary; - color: $color-primary-fg; -} - -.fileinfo .is_file_replace { - width: 100%; - height: 100%; - background: $color-gray-lighter; -} - -.file_selection_ctrl button.select_item { - display: inline; - background: $color-bg; - padding: 9px 0px 9px 0px; - margin-left: 0px; - margin-right: -7px; - min-width: 30px; - border-top-left-radius: 0; - border-bottom-left-radius: 0; -} - - -.cap_select_file { - cursor: pointer; -} - -.cap_select_file:hover { - color: $grid-hover-fg-color !important; - .fm_folder_list, - .fm_folder_grid, - .fm_file_grid, - .fm_file_list { - color: $grid-hover-fg-color !important; - } -} - -.add-folder-icon { - position: relative; - top: -8px; - left: -6px; - font-size: 8px; - margin-right: -7px; -} - -table.tablesorter { - th:focus, - tr:focus { - outline: $input-focus-border-color auto 5px !important; - } -} - -#contents { - li:focus { - outline: $input-focus-border-color auto 5px !important; - } -} - -/* Specific to IE11 where we want to highlight the focus on grid/row */ -@media all and (-ms-high-contrast: none), (-ms-high-contrast: active) { - table.tablesorter { - th:focus, - tr:focus { - border: 2px solid $input-focus-border-color !important; - } - } - - #contents { - li:focus { - border: 2px solid $input-focus-border-color !important; - } - } -} diff --git a/web/pgadmin/settings/__init__.py b/web/pgadmin/settings/__init__.py index 178c546a8..688d442a1 100644 --- a/web/pgadmin/settings/__init__.py +++ b/web/pgadmin/settings/__init__.py @@ -23,6 +23,7 @@ from pgadmin.utils.menu import MenuItem from pgadmin.model import db, Setting from pgadmin.utils.constants import MIMETYPE_APP_JS +from .utils import get_dialog_type, get_file_type_setting MODULE_NAME = 'settings' @@ -223,38 +224,20 @@ def get_browser_tree_state(): mimetype="application/json") -def _get_dialog_type(file_type): - """ - This function return dialog type - :param file_type: - :return: dialog type. - """ - if 'pgerd' in file_type: - return 'erd_file_type' - elif 'backup' in file_type: - return 'backup_file_type' - elif 'csv' in file_type and 'txt' in file_type: - return 'import_export_file_type' - elif 'csv' in file_type and 'txt' not in file_type: - return 'storage_manager_file_type' - else: - return 'sqleditor_file_format' - - @blueprint.route("/save_file_format_setting/", endpoint="save_file_format_setting", methods=['POST']) @login_required def save_file_format_setting(): """ - This function save the selected file format. + This function save the selected file format.save_file_format_setting :return: save file format response """ data = request.form if request.form else json.loads( request.data.decode('utf-8')) - file_type = _get_dialog_type(data['allowed_file_types']) + file_type = get_dialog_type(data['allowed_file_types']) - store_setting(file_type, data['selectedFormat']) + store_setting(file_type, data['last_selected_format']) return make_json_response(success=True, info=data, result=request.form) @@ -276,11 +259,5 @@ def get_file_format_setting(): except (ValueError, TypeError, KeyError): data[k] = v - file_type = _get_dialog_type(list(data.values())) - - data = Setting.query.filter_by( - user_id=current_user.id, setting=file_type).first() - if data is None: - return make_json_response(success=True, info='*') - else: - return make_json_response(success=True, info=data.value) + return make_json_response(success=True, + info=get_file_type_setting(list(data.values()))) diff --git a/web/pgadmin/settings/utils.py b/web/pgadmin/settings/utils.py new file mode 100644 index 000000000..69d8e9792 --- /dev/null +++ b/web/pgadmin/settings/utils.py @@ -0,0 +1,36 @@ +from flask_login import current_user +from pgadmin.model import Setting + + +def get_dialog_type(file_type): + """ + This function return dialog type + :param file_type: + :return: dialog type. + """ + if 'pgerd' in file_type: + return 'erd_file_type' + elif 'backup' in file_type: + return 'backup_file_type' + elif 'csv' in file_type and 'txt' in file_type: + return 'import_export_file_type' + elif 'csv' in file_type and 'txt' not in file_type: + return 'storage_manager_file_type' + else: + return 'sqleditor_file_format' + + +def get_file_type_setting(file_types): + """ + This function return last file format setting based on file types + :param file_types: + :return: file format setting. + """ + file_type = get_dialog_type(list(file_types)) + + data = Setting.query.filter_by( + user_id=current_user.id, setting=file_type).first() + if data is None: + return '*' + else: + return data.value diff --git a/web/pgadmin/static/js/SchemaView/DataGridView.jsx b/web/pgadmin/static/js/SchemaView/DataGridView.jsx index 914ac5dd3..9d06a5430 100644 --- a/web/pgadmin/static/js/SchemaView/DataGridView.jsx +++ b/web/pgadmin/static/js/SchemaView/DataGridView.jsx @@ -25,11 +25,11 @@ import _ from 'lodash'; import gettext from 'sources/gettext'; import { SCHEMA_STATE_ACTIONS, StateUtilsContext } from '.'; import FormView, { getFieldMetaData } from './FormView'; -import { confirmDeleteRow } from '../helpers/legacyConnector'; import CustomPropTypes from 'sources/custom_prop_types'; import { evalFunc } from 'sources/utils'; import { DepListenerContext } from './DepListener'; import { useIsMounted } from '../custom_hooks'; +import Notify from '../helpers/Notifier'; const useStyles = makeStyles((theme)=>({ grid: { @@ -303,15 +303,21 @@ export default function DataGridView({ return ( } onClick={()=>{ - confirmDeleteRow(()=>{ - /* Get the changes on dependent fields as well */ - dataDispatch({ - type: SCHEMA_STATE_ACTIONS.DELETE_ROW, - path: accessPath, - value: row.index, - }); - - }, ()=>{/*This is intentional (SonarQube)*/}, props.customDeleteTitle, props.customDeleteMsg); + Notify.confirm( + props.customDeleteTitle || gettext('Delete Row'), + props.customDeleteMsg || gettext('Are you sure you wish to delete this row?'), + function() { + dataDispatch({ + type: SCHEMA_STATE_ACTIONS.DELETE_ROW, + path: accessPath, + value: row.index, + }); + return true; + }, + function() { + return true; + } + ); }} className={classes.gridRowButton} disabled={!canDeleteRow} /> ); } diff --git a/web/pgadmin/static/js/Theme/index.jsx b/web/pgadmin/static/js/Theme/index.jsx index 1f6be3d4c..4b966f881 100644 --- a/web/pgadmin/static/js/Theme/index.jsx +++ b/web/pgadmin/static/js/Theme/index.jsx @@ -125,6 +125,9 @@ basicSettings = createMuiTheme(basicSettings, { }, adornedEnd: { paddingRight: basicSettings.spacing(0.75), + }, + marginDense: { + height: '28px', } }, MuiAccordion: { diff --git a/web/pgadmin/static/js/backform.pgadmin.js b/web/pgadmin/static/js/backform.pgadmin.js index 0f0a8ab4d..3368111cf 100644 --- a/web/pgadmin/static/js/backform.pgadmin.js +++ b/web/pgadmin/static/js/backform.pgadmin.js @@ -2835,106 +2835,6 @@ define([ ].join('\n')), }); - /* - * Input File Control: This control is used with Storage Manager Dialog, - * It allows user to perform following operations: - * - Select File - * - Select Folder - * - Create File - * - Opening Storage Manager Dialog itself. - */ - Backform.FileControl = Backform.InputControl.extend({ - defaults: { - type: 'text', - label: '', - min: undefined, - max: undefined, - maxlength: 255, - extraClasses: [], - dialog_title: '', - btn_primary: '', - helpMessage: null, - dialog_type: 'select_file', - }, - initialize: function() { - Backform.InputControl.prototype.initialize.apply(this, arguments); - }, - template: _.template([ - '', - '
    ', - '
    ', - ' <%=readonly ? "readonly aria-readonly=true" : ""%> <%=required ? "required" : ""%> />', - '
    ', - '', - '
    ', - '
    ', - '<% if (helpMessage && helpMessage.length) { %>', - '<%=helpMessage%>', - '<% } %>', - '
    ', - ].join('\n')), - events: function() { - // Inherit all default events of InputControl - return _.extend({}, Backform.InputControl.prototype.events, { - 'click .select_item': 'onSelect', - }); - }, - onSelect: function() { - var dialog_type = this.field.get('dialog_type'), - supp_types = this.field.get('supp_types'), - btn_primary = this.field.get('btn_primary'), - dialog_title = this.field.get('dialog_title'), - params = { - supported_types: supp_types, - dialog_type: dialog_type, - dialog_title: dialog_title, - btn_primary: btn_primary, - }; - - pgAdmin.FileManager.init(); - pgAdmin.FileManager.show_dialog(params); - // Listen click events of Storage Manager dialog buttons - this.listen_file_dlg_events(); - }, - storage_dlg_hander: function(value) { - var attrArr = this.field.get('name').split('.'), - name = attrArr.shift(); - - this.remove_file_dlg_event_listeners(); - - // Set selected value into the model - this.model.set(name, decodeURI(value)); - this.$el.find('input[type=text]').focus(); - }, - storage_close_dlg_hander: function() { - this.remove_file_dlg_event_listeners(); - }, - listen_file_dlg_events: function() { - pgAdmin.Browser.Events.on('pgadmin-storage:finish_btn:' + this.field.get('dialog_type'), this.storage_dlg_hander, this); - pgAdmin.Browser.Events.on('pgadmin-storage:cancel_btn:' + this.field.get('dialog_type'), this.storage_close_dlg_hander, this); - }, - remove_file_dlg_event_listeners: function() { - pgAdmin.Browser.Events.off('pgadmin-storage:finish_btn:' + this.field.get('dialog_type'), this.storage_dlg_hander, this); - pgAdmin.Browser.Events.off('pgadmin-storage:cancel_btn:' + this.field.get('dialog_type'), this.storage_close_dlg_hander, this); - }, - clearInvalid: function() { - Backform.InputControl.prototype.clearInvalid.apply(this, arguments); - this.$el.removeClass('pgadmin-file-has-error'); - return this; - }, - updateInvalid: function() { - Backform.InputControl.prototype.updateInvalid.apply(this, arguments); - // Introduce a new class to fix the error icon placement on the control - this.$el.addClass('pgadmin-file-has-error'); - }, - disable_button: function() { - this.$el.find('button.select_item').attr('disabled', 'disabled'); - }, - enable_button: function() { - this.$el.find('button.select_item').removeAttr('disabled'); - }, - }); - Backform.DatetimepickerControl = Backform.InputControl.extend({ defaults: { diff --git a/web/pgadmin/static/js/backgrid.pgadmin.js b/web/pgadmin/static/js/backgrid.pgadmin.js index b997a0c69..0eadfa606 100644 --- a/web/pgadmin/static/js/backgrid.pgadmin.js +++ b/web/pgadmin/static/js/backgrid.pgadmin.js @@ -12,10 +12,10 @@ import Notify from '../../static/js/helpers/Notifier'; define([ 'sources/gettext', 'underscore', 'jquery', 'backbone', 'backform', 'backgrid', 'alertify', 'moment', 'bignumber', 'codemirror', 'sources/utils', 'sources/keyboard_shortcuts', 'sources/select2/configure_show_on_scroll', - 'sources/window', 'sources/url_for', 'bootstrap.datetimepicker', 'backgrid.filter', 'bootstrap.toggle', + 'sources/window', 'bootstrap.datetimepicker', 'backgrid.filter', 'bootstrap.toggle', ], function( gettext, _, $, Backbone, Backform, Backgrid, Alertify, moment, BigNumber, CodeMirror, - commonUtils, keyboardShortcuts, configure_show_on_scroll, pgWindow, url_for + commonUtils, keyboardShortcuts, configure_show_on_scroll, pgWindow ) { /* * Add mechanism in backgrid to render different types of cells in @@ -2314,125 +2314,5 @@ define([ }, }); - Backgrid.Extension.SelectFileCell = Backgrid.Cell.extend({ - /** @property */ - className: 'file-cell', - defaults: { - supported_types: ['*'], - dialog_type: 'select_file', - dialog_title: gettext('Select file'), - type: 'text', - value: '', - placeholder: gettext('Select file...'), - disabled: false, - browse_btn_label: gettext('Select file'), - check_btn_label: gettext('Validate file'), - browse_btn_visible: true, - validate_btn_visible: true, - }, - - initialize: function() { - Backgrid.Cell.prototype.initialize.apply(this, arguments); - this.data = _.extend(this.defaults, this.column.toJSON()); - }, - template: _.template([ - '
    ', - ' />', - '<% if (browse_btn_visible) { %>', - '
    ', - '', - '
    ', - '<% } %>', - '<% if (validate_btn_visible) { %>', - '
    ', - '', - '
    ', - '<% } %>', - '
    ', - ].join('\n')), - events: { - 'change input': 'onChange', - 'click .select_item': 'onSelect', - 'click .validate_item': 'onValidate', - }, - - render: function() { - this.$el.empty(); - this.data = _.extend(this.data, {value: this.model.get(this.column.get('name'))}); - // Adding unique id - this.data['cId'] = _.uniqueId('pgC_'); - this.$el.append(this.template(this.data)); - - this.$input = this.$el.find('input'); - this.delegateEvents(); - - return this; - }, - onChange: function() { - var model = this.model, - column = this.column, - val = this.formatter.toRaw(this.$input.prop('value'), model); - - model.set(column.get('name'), val); - }, - onSelect: function() { - let self = this; - - var params = { - supported_types: self.data.supported_types, - dialog_type: self.data.dialog_type, - dialog_title: self.data.dialog_title - }; - - pgAdmin.FileManager.init(); - pgAdmin.FileManager.show_dialog(params); - // Listen click events of Storage Manager dialog buttons - this.listen_file_dlg_events(); - }, - storage_dlg_hander: function(value) { - var attrArr = this.column.get('name').split('.'), - name = attrArr.shift(); - - this.remove_file_dlg_event_listeners(); - - // Set selected value into the model - this.model.set(name, decodeURI(value)); - }, - storage_close_dlg_hander: function() { - this.remove_file_dlg_event_listeners(); - }, - listen_file_dlg_events: function() { - pgAdmin.Browser.Events.on('pgadmin-storage:finish_btn:' + this.data.dialog_type, this.storage_dlg_hander, this); - pgAdmin.Browser.Events.on('pgadmin-storage:cancel_btn:' + this.data.dialog_type, this.storage_close_dlg_hander, this); - }, - remove_file_dlg_event_listeners: function() { - pgAdmin.Browser.Events.off('pgadmin-storage:finish_btn:' + this.data.dialog_type, this.storage_dlg_hander, this); - pgAdmin.Browser.Events.off('pgadmin-storage:cancel_btn:' + this.data.dialog_type, this.storage_close_dlg_hander, this); - }, - onValidate: function() { - var model = this.model, - val = this.formatter.toRaw(this.$input.prop('value'), model); - - if (_.isNull(val) || val.trim() === '') { - Notify.alert(gettext('Validate Path'), gettext('Path should not be empty.')); - } - - $.ajax({ - url: url_for('misc.validate_binary_path'), - method: 'POST', - contentType: 'application/json', - data: JSON.stringify({ - 'utility_path': val, - }), - }) - .done(function(res) { - Notify.alert(gettext('Validate binary path'), gettext(res.data)); - }) - .fail(function(xhr, error) { - Notify.pgNotifier(error, xhr, gettext('Failed to validate binary path.')); - }); - }, - }); - return Backgrid; }); diff --git a/web/pgadmin/static/js/components/Buttons.jsx b/web/pgadmin/static/js/components/Buttons.jsx index c15e313d5..c446926c5 100644 --- a/web/pgadmin/static/js/components/Buttons.jsx +++ b/web/pgadmin/static/js/components/Buttons.jsx @@ -199,7 +199,7 @@ PgIconButton.propTypes = { export const PgButtonGroup = forwardRef(({children, ...props}, ref)=>{ /* Tooltip does not work for disabled items */ return ( - + {children} ); diff --git a/web/pgadmin/static/js/components/FormComponents.jsx b/web/pgadmin/static/js/components/FormComponents.jsx index cb78020b1..2dcd79d69 100644 --- a/web/pgadmin/static/js/components/FormComponents.jsx +++ b/web/pgadmin/static/js/components/FormComponents.jsx @@ -35,13 +35,13 @@ import * as DateFns from 'date-fns'; import CodeMirror from './CodeMirror'; import gettext from 'sources/gettext'; -import { showFileDialog } from '../helpers/legacyConnector'; import _ from 'lodash'; import { DefaultButton, PrimaryButton, PgIconButton } from './Buttons'; import CustomPropTypes from '../custom_prop_types'; import KeyboardShortcuts from './KeyboardShortcuts'; import QueryThresholds from './QueryThresholds'; import SelectThemes from './SelectThemes'; +import { showFileManager } from '../helpers/showFileManager'; const useStyles = makeStyles((theme) => ({ @@ -326,11 +326,10 @@ FormInputDateTimePicker.propTypes = { /* Use forwardRef to pass ref prop to OutlinedInput */ export const InputText = forwardRef(({ - cid, helpid, readonly, disabled, value, onChange, controlProps, type, ...props }, ref) => { + cid, helpid, readonly, disabled, value, onChange, controlProps, type, size, ...props }, ref) => { const maxlength = typeof(controlProps?.maxLength) != 'undefined' ? controlProps.maxLength : 255; - const classes = useStyles(); const patterns = { 'numeric': '^-?[0-9]\\d*\\.?\\d*$', 'int': '^-?[0-9]\\d*$', @@ -356,12 +355,17 @@ export const InputText = forwardRef(({ finalValue = controlProps.formatter.fromRaw(finalValue); } + const filteredProps = _.pickBy(props, (_v, key)=>( + /* When used in ButtonGroup, following props should be skipped */ + !['color', 'disableElevation', 'disableFocusRipple', 'disableRipple'].includes(key) + )); + return ( -1 ? { type: 'tel' } : { type: type })} /> ); @@ -394,6 +398,7 @@ InputText.propTypes = { onChange: PropTypes.func, controlProps: PropTypes.object, type: PropTypes.string, + size: PropTypes.string, }; export function FormInputText({ hasError, required, label, className, helpMessage, testcid, ...props }) { @@ -412,7 +417,6 @@ FormInputText.propTypes = { testcid: PropTypes.string, }; -/* Using the existing file dialog functions using showFileDialog */ export function InputFileSelect({ controlProps, onChange, disabled, readonly, isvalidate = false, hideBrowseButton=false,validate, ...props }) { const inpRef = useRef(); let textControlProps = {}; @@ -420,15 +424,24 @@ export function InputFileSelect({ controlProps, onChange, disabled, readonly, is const {placeholder} = controlProps; textControlProps = {placeholder}; } - const onFileSelect = (value) => { - onChange && onChange(decodeURI(value)); - inpRef.current.focus(); + const showFileDialog = ()=>{ + let params = { + supported_types: controlProps.supportedTypes || [], + dialog_type: controlProps.dialogType || 'select_file', + dialog_title: controlProps.dialogTitle || '', + btn_primary: controlProps.btnPrimary || '', + }; + showFileManager(params, (fileName)=>{ + onChange && onChange(decodeURI(fileName)); + inpRef.current.focus(); + }); }; + return ( {!hideBrowseButton && - showFileDialog(controlProps, onFileSelect)} + } {isvalidate && @@ -1184,6 +1197,9 @@ const useStylesFormFooter = makeStyles((theme) => ({ message: { marginLeft: theme.spacing(0.5), }, + messageCenter: { + margin: 'auto', + }, closeButton: { marginLeft: 'auto', }, @@ -1272,13 +1288,13 @@ FormInputSelectThemes.propTypes = { }; -export function NotifierMessage({ type = MESSAGE_TYPE.SUCCESS, message, closable = true, onClose = () => {/*This is intentional (SonarQube)*/ } }) { +export function NotifierMessage({ type = MESSAGE_TYPE.SUCCESS, message, closable = true, showIcon=true, textCenter=false, onClose = () => {/*This is intentional (SonarQube)*/ } }) { const classes = useStylesFormFooter(); return ( - - {HTMLReactParse(message || '')} + {showIcon && } + {HTMLReactParse(message || '')} {closable && } @@ -1290,6 +1306,8 @@ NotifierMessage.propTypes = { type: PropTypes.oneOf(Object.values(MESSAGE_TYPE)).isRequired, message: PropTypes.string, closable: PropTypes.bool, + showIcon: PropTypes.bool, + textCenter: PropTypes.bool, onClose: PropTypes.func, }; diff --git a/web/pgadmin/static/js/components/PgReactDataGrid.jsx b/web/pgadmin/static/js/components/PgReactDataGrid.jsx new file mode 100644 index 000000000..210d8c980 --- /dev/null +++ b/web/pgadmin/static/js/components/PgReactDataGrid.jsx @@ -0,0 +1,88 @@ +import React from 'react'; +import ReactDataGrid from 'react-data-grid'; +import { makeStyles } from '@material-ui/core'; +import clsx from 'clsx'; +import PropTypes from 'prop-types'; +import CustomPropTypes from '../custom_prop_types'; + +const useStyles = makeStyles((theme)=>({ + root: { + height: '100%', + color: theme.palette.text.primary, + backgroundColor: theme.otherVars.qtDatagridBg, + fontSize: '12px', + border: 'none', + '--rdg-selection-color': theme.palette.primary.main, + '& .rdg-cell': { + ...theme.mixins.panelBorder.right, + ...theme.mixins.panelBorder.bottom, + fontWeight: 'abc', + '&[aria-colindex="1"]': { + padding: 0, + }, + '&[aria-selected=true]:not([role="columnheader"])': { + outlineWidth: '0px', + outlineOffset: '0px', + } + }, + '& .rdg-header-row .rdg-cell': { + padding: 0, + }, + '& .rdg-header-row': { + backgroundColor: theme.palette.background.default, + fontWeight: 'normal', + }, + '& .rdg-row': { + backgroundColor: theme.palette.background.default, + '&[aria-selected=true]': { + backgroundColor: theme.palette.primary.light, + color: theme.otherVars.qtDatagridSelectFg, + }, + } + }, + cellSelection: { + '& .rdg-cell': { + '&[aria-selected=true]:not([role="columnheader"])': { + outlineWidth: '1px', + outlineOffset: '-1px', + backgroundColor: theme.palette.primary.light, + color: theme.otherVars.qtDatagridSelectFg, + } + }, + }, + hasSelectColumn: { + '& .rdg-cell': { + '&[aria-selected=true][aria-colindex="1"]': { + outlineWidth: '2px', + outlineOffset: '-2px', + backgroundColor: theme.otherVars.qtDatagridBg, + color: theme.palette.text.primary, + } + }, + '& .rdg-row[aria-selected=true] .rdg-cell:nth-child(1)': { + backgroundColor: theme.palette.primary.main, + color: theme.palette.primary.contrastText, + } + } +})); + + +export default function PgReactDataGrid({gridRef, className, hasSelectColumn=true, ...props}) { + const classes = useStyles(); + let finalClassName = [classes.root]; + hasSelectColumn && finalClassName.push(classes.hasSelectColumn); + props.enableCellSelect && finalClassName.push(classes.cellSelection); + finalClassName.push(className); + return ; +} + +PgReactDataGrid.propTypes = { + gridRef: CustomPropTypes.ref, + className: CustomPropTypes.className, + hasSelectColumn: PropTypes.bool, + enableCellSelect: PropTypes.bool, +}; diff --git a/web/pgadmin/static/js/helpers/ModalProvider.jsx b/web/pgadmin/static/js/helpers/ModalProvider.jsx index 80523fe04..510cb96dc 100644 --- a/web/pgadmin/static/js/helpers/ModalProvider.jsx +++ b/web/pgadmin/static/js/helpers/ModalProvider.jsx @@ -22,7 +22,7 @@ import CheckRoundedIcon from '@material-ui/icons/CheckRounded'; import { Rnd } from 'react-rnd'; import { ExpandDialogIcon, MinimizeDialogIcon } from '../components/ExternalIcon'; -const ModalContext = React.createContext({}); +export const ModalContext = React.createContext({}); const MIN_HEIGHT = 190; const MIN_WIDTH = 500; diff --git a/web/pgadmin/static/js/helpers/legacyConnector.js b/web/pgadmin/static/js/helpers/legacyConnector.js deleted file mode 100644 index adc7c27d6..000000000 --- a/web/pgadmin/static/js/helpers/legacyConnector.js +++ /dev/null @@ -1,65 +0,0 @@ -///////////////////////////////////////////////////////////// -// -// pgAdmin 4 - PostgreSQL Tools -// -// Copyright (C) 2013 - 2022, The pgAdmin Development Team -// This software is released under the PostgreSQL Licence -// -////////////////////////////////////////////////////////////// - -/* This file will have wrappers and connectors used by React components to - * re-use any existing non-react components. - * These functions may not be needed once all are migrated - */ - -import gettext from 'sources/gettext'; -import pgAdmin from 'sources/pgadmin'; -import Notify from './Notifier'; - -export function confirmDeleteRow(onOK, onCancel, title, message) { - Notify.confirm( - title || gettext('Delete Row'), - message || gettext('Are you sure you wish to delete this row?'), - function() { - onOK(); - return true; - }, - function() { - onCancel(); - return true; - } - ); -} - -/* Used by file select component to re-use existing logic */ -export function showFileDialog(dialogParams, onFileSelect) { - let params = { - supported_types: dialogParams.supportedTypes || [], - dialog_type: dialogParams.dialogType || 'select_file', - dialog_title: dialogParams.dialogTitle || '', - btn_primary: dialogParams.btnPrimary || '', - }; - pgAdmin.FileManager.init(); - pgAdmin.FileManager.show_dialog(params); - - const onFileSelectClose = (value)=>{ - removeListeners(); - onFileSelect(value); - }; - const onDialogClose = ()=>removeListeners(); - pgAdmin.Browser.Events.on('pgadmin-storage:finish_btn:' + params.dialog_type, onFileSelectClose); - pgAdmin.Browser.Events.on('pgadmin-storage:cancel_btn:' + params.dialog_type, onDialogClose); - - const removeListeners = ()=>{ - pgAdmin.Browser.Events.off('pgadmin-storage:finish_btn:' + params.dialog_type, onFileSelectClose); - pgAdmin.Browser.Events.off('pgadmin-storage:cancel_btn:' + params.dialog_type, onDialogClose); - }; -} - -export function onPgadminEvent(eventName, handler) { - pgAdmin.Browser.Events.on(eventName, handler); -} - -export function offPgadminEvent(eventName, handler) { - pgAdmin.Browser.Events.off(eventName, handler); -} diff --git a/web/pgadmin/static/js/helpers/showFileManager.js b/web/pgadmin/static/js/helpers/showFileManager.js new file mode 100644 index 000000000..fc658920c --- /dev/null +++ b/web/pgadmin/static/js/helpers/showFileManager.js @@ -0,0 +1,6 @@ +import pgAdmin from 'sources/pgadmin'; +import 'pgadmin.tools.file_manager'; + +export function showFileManager(...args) { + pgAdmin.Tools.FileManager.show(...args); +} diff --git a/web/pgadmin/static/js/tree/tree.js b/web/pgadmin/static/js/tree/tree.js index 839016adf..467822b9f 100644 --- a/web/pgadmin/static/js/tree/tree.js +++ b/web/pgadmin/static/js/tree/tree.js @@ -14,8 +14,6 @@ import pgAdmin from 'sources/pgadmin'; import { FileType } from 'react-aspen'; import { TreeNode } from './tree_nodes'; -import { isValidData } from 'sources/utils'; - function manageTreeEvents(event, eventName, item) { let d = item ? item._metadata.data : []; let node_metadata = item ? item._metadata : {}; @@ -594,6 +592,6 @@ export function findInTree(rootNode, path) { })(rootNode); } -let isValidTreeNodeData = isValidData; +let isValidTreeNodeData = (data) => (!_.isEmpty(data)); export { isValidTreeNodeData }; diff --git a/web/pgadmin/static/js/utils.js b/web/pgadmin/static/js/utils.js index 0602921d4..5f5134834 100644 --- a/web/pgadmin/static/js/utils.js +++ b/web/pgadmin/static/js/utils.js @@ -7,7 +7,7 @@ // ////////////////////////////////////////////////////////////////////////// -import _ from 'underscore'; +import _ from 'lodash'; import $ from 'jquery'; import gettext from 'sources/gettext'; import 'wcdocker'; @@ -115,14 +115,6 @@ export function findAndSetFocus(container) { }, 200); } -let isValidData = (data) => (!_.isUndefined(data) && !_.isNull(data)); -let isFunction = (fn) => (_.isFunction(fn)); -let isString = (str) => (_.isString(str)); - -export { - isValidData, isFunction, isString, -}; - export function getEpoch(inp_date) { let date_obj = inp_date ? inp_date : new Date(); return parseInt(date_obj.getTime()/1000); @@ -456,6 +448,10 @@ export function getBrowser() { tem=/\brv[ :]+(\d+)/g.exec(ua) || []; return {name:'IE', version:(tem[1]||'')}; } + if(ua.startsWith('Nwjs')) { + let nwjs = ua.split('-')[0]?.split(':'); + return {name:nwjs[0], version: nwjs[1]}; + } if(M[1]==='Chrome') { tem=ua.match(/\bOPR|Edge\/(\d+)/); @@ -480,3 +476,21 @@ export function checkTrojanSource(content, isPasteEvent) { Notify.alert(gettext('Trojan Source Warning'), msg); } } + +export function downloadBlob(blob, fileName) { + let urlCreator = window.URL || window.webkitURL, + downloadUrl = urlCreator.createObjectURL(blob), + link = document.createElement('a'); + + document.body.appendChild(link); + + if (getBrowser() === 'IE' && window.navigator.msSaveBlob) { + // IE10+ : (has Blob, but not a[download] or URL) + window.navigator.msSaveBlob(blob, fileName); + } else { + link.setAttribute('href', downloadUrl); + link.setAttribute('download', fileName); + link.click(); + } + document.body.removeChild(link); +} diff --git a/web/pgadmin/tools/__init__.py b/web/pgadmin/tools/__init__.py index 711699a89..9ab20d574 100644 --- a/web/pgadmin/tools/__init__.py +++ b/web/pgadmin/tools/__init__.py @@ -64,9 +64,6 @@ class ToolsModule(PgAdminModule): from .sqleditor import blueprint as module app.register_blueprint(module) - from .storage_manager import blueprint as module - app.register_blueprint(module) - from .user_management import blueprint as module app.register_blueprint(module) diff --git a/web/pgadmin/tools/erd/static/js/erd_tool/index.js b/web/pgadmin/tools/erd/static/js/erd_tool/index.js index 8563ac562..1c7a10d12 100644 --- a/web/pgadmin/tools/erd/static/js/erd_tool/index.js +++ b/web/pgadmin/tools/erd/static/js/erd_tool/index.js @@ -17,6 +17,9 @@ import Alertify from 'pgadmin.alertifyjs'; import pgWindow from 'sources/window'; import pgAdmin from 'sources/pgadmin'; +import ModalProvider from '../../../../../static/js/helpers/ModalProvider'; +import Theme from '../../../../../static/js/Theme'; + export default class ERDTool { constructor(container, params) { this.container = document.querySelector(container); @@ -37,13 +40,17 @@ export default class ERDTool { }); ReactDOM.render( - , + + + + + , this.container ); } diff --git a/web/pgadmin/tools/erd/static/js/erd_tool/ui_components/BodyWidget.jsx b/web/pgadmin/tools/erd/static/js/erd_tool/ui_components/BodyWidget.jsx index 4132ae1e9..6427292d3 100644 --- a/web/pgadmin/tools/erd/static/js/erd_tool/ui_components/BodyWidget.jsx +++ b/web/pgadmin/tools/erd/static/js/erd_tool/ui_components/BodyWidget.jsx @@ -27,6 +27,7 @@ import 'wcdocker'; import Theme from '../../../../../../static/js/Theme'; import TableSchema from '../../../../../../browser/server_groups/servers/databases/schemas/tables/static/js/table.ui'; import Notify from '../../../../../../static/js/helpers/Notifier'; +import { ModalContext } from '../../../../../../static/js/helpers/ModalProvider'; /* Custom react-diagram action for keyboard events */ export class KeyboardShortcutAction extends Action { @@ -61,6 +62,7 @@ export class KeyboardShortcutAction extends Action { /* The main body container for the ERD */ export default class BodyWidget extends React.Component { + static contextType = ModalContext; constructor() { super(); this.state = { @@ -214,8 +216,6 @@ export default class BodyWidget extends React.Component { backgroundPosition: '0px 0px', }); - this.props.pgAdmin.Browser.Events.on('pgadmin-storage:finish_btn:select_file', this.openFile, this); - this.props.pgAdmin.Browser.Events.on('pgadmin-storage:finish_btn:create_file', this.saveFile, this); this.props.pgAdmin.Browser.onPreferencesChange('erd', () => { this.setState({ preferences: this.props.pgWindow.pgAdmin.Browser.get_preferences_for_module('erd'), @@ -468,11 +468,10 @@ export default class BodyWidget extends React.Component { onLoadDiagram() { var params = { - 'supported_types': ['pgerd'], // file types allowed + 'supported_types': ['*','pgerd'], // file types allowed 'dialog_type': 'select_file', // open select file dialog }; - this.props.pgAdmin.FileManager.init(); - this.props.pgAdmin.FileManager.show_dialog(params); + this.props.pgAdmin.Tools.FileManager.show(params, this.openFile.bind(this), null, this.context); } openFile(fileName) { @@ -501,13 +500,12 @@ export default class BodyWidget extends React.Component { this.saveFile(this.state.current_file); } else { var params = { - 'supported_types': ['pgerd'], + 'supported_types': ['*','pgerd'], 'dialog_type': 'create_file', 'dialog_title': 'Save File', 'btn_primary': 'Save', }; - this.props.pgAdmin.FileManager.init(); - this.props.pgAdmin.FileManager.show_dialog(params); + this.props.pgAdmin.Tools.FileManager.show(params, this.saveFile.bind(this), null, this.context); } } diff --git a/web/pgadmin/tools/erd/static/js/erd_tool_hook.js b/web/pgadmin/tools/erd/static/js/erd_tool_hook.js index 9fa0a2e3a..dafc69afd 100644 --- a/web/pgadmin/tools/erd/static/js/erd_tool_hook.js +++ b/web/pgadmin/tools/erd/static/js/erd_tool_hook.js @@ -10,7 +10,7 @@ define([ 'sources/pgadmin', 'pgadmin.tools.erd/erd_tool', 'pgadmin.browser', 'pgadmin.browser.server.privilege', 'pgadmin.node.database', 'pgadmin.node.primary_key', - 'pgadmin.node.foreign_key', 'pgadmin.browser.datamodel', 'pgadmin.file_manager', + 'pgadmin.node.foreign_key', 'pgadmin.browser.datamodel', 'pgadmin.tools.file_manager', ], function( pgAdmin, ERDToolModule ) { diff --git a/web/pgadmin/tools/import_export_servers/static/js/import_export_servers.js b/web/pgadmin/tools/import_export_servers/static/js/import_export_servers.js index d2953a3d2..b436b79cf 100644 --- a/web/pgadmin/tools/import_export_servers/static/js/import_export_servers.js +++ b/web/pgadmin/tools/import_export_servers/static/js/import_export_servers.js @@ -9,7 +9,6 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import 'pgadmin.file_manager'; import gettext from 'sources/gettext'; import Alertify from 'pgadmin.alertifyjs'; import Theme from 'sources/Theme'; diff --git a/web/pgadmin/tools/sqleditor/static/js/SQLEditorModule.js b/web/pgadmin/tools/sqleditor/static/js/SQLEditorModule.js index fcf606254..33587d7e5 100644 --- a/web/pgadmin/tools/sqleditor/static/js/SQLEditorModule.js +++ b/web/pgadmin/tools/sqleditor/static/js/SQLEditorModule.js @@ -20,8 +20,8 @@ var wcDocker = window.wcDocker; import pgWindow from 'sources/window'; import pgAdmin from 'sources/pgadmin'; import pgBrowser from 'pgadmin.browser'; -import 'pgadmin.file_manager'; import 'pgadmin.tools.user_management'; +import 'pgadmin.tools.file_manager'; import gettext from 'sources/gettext'; import React from 'react'; import ReactDOM from 'react-dom'; diff --git a/web/pgadmin/tools/sqleditor/static/js/components/QueryToolComponent.jsx b/web/pgadmin/tools/sqleditor/static/js/components/QueryToolComponent.jsx index 71b5bb16c..86966c73b 100644 --- a/web/pgadmin/tools/sqleditor/static/js/components/QueryToolComponent.jsx +++ b/web/pgadmin/tools/sqleditor/static/js/components/QueryToolComponent.jsx @@ -332,15 +332,6 @@ export default function QueryToolComponent({params, pgWindow, pgAdmin, selectedN } } }); - - pgAdmin.Browser.Events.on('pgadmin-storage:finish_btn:select_file', (fileName)=>{ - eventBus.current.fireEvent(QUERY_TOOL_EVENTS.LOAD_FILE, fileName); - }, pgAdmin); - - pgAdmin.Browser.Events.on('pgadmin-storage:finish_btn:create_file', (fileName)=>{ - eventBus.current.fireEvent(QUERY_TOOL_EVENTS.SAVE_FILE, fileName); - }, pgAdmin); - window.addEventListener('beforeunload', onBeforeUnload); }, []); @@ -428,8 +419,9 @@ export default function QueryToolComponent({params, pgWindow, pgAdmin, selectedN 'supported_types': ['*', 'sql'], // file types allowed 'dialog_type': 'select_file', // open select file dialog }; - pgAdmin.FileManager.init(); - pgAdmin.FileManager.show_dialog(fileParams); + pgAdmin.Tools.FileManager.show(fileParams, (fileName)=>{ + eventBus.current.fireEvent(QUERY_TOOL_EVENTS.LOAD_FILE, fileName); + }, null, modal); }], [QUERY_TOOL_EVENTS.TRIGGER_SAVE_FILE, (isSaveAs=false)=>{ if(!isSaveAs && qtState.current_file) { @@ -441,8 +433,9 @@ export default function QueryToolComponent({params, pgWindow, pgAdmin, selectedN 'dialog_title': 'Save File', 'btn_primary': 'Save', }; - pgAdmin.FileManager.init(); - pgAdmin.FileManager.show_dialog(fileParams); + pgAdmin.Tools.FileManager.show(fileParams, (fileName)=>{ + eventBus.current.fireEvent(QUERY_TOOL_EVENTS.SAVE_FILE, fileName); + }, null, modal); } }], [QUERY_TOOL_EVENTS.LOAD_FILE_DONE, fileDone], diff --git a/web/pgadmin/tools/sqleditor/static/js/components/QueryToolDataGrid/index.jsx b/web/pgadmin/tools/sqleditor/static/js/components/QueryToolDataGrid/index.jsx index 5e51c6b4b..23c42a711 100644 --- a/web/pgadmin/tools/sqleditor/static/js/components/QueryToolDataGrid/index.jsx +++ b/web/pgadmin/tools/sqleditor/static/js/components/QueryToolDataGrid/index.jsx @@ -9,7 +9,7 @@ import { Box, makeStyles } from '@material-ui/core'; import _ from 'lodash'; import React, {useState, useEffect, useContext, useRef, useLayoutEffect} from 'react'; -import ReactDataGrid, {Row, useRowSelection} from 'react-data-grid'; +import {Row, useRowSelection} from 'react-data-grid'; import LockIcon from '@material-ui/icons/Lock'; import EditIcon from '@material-ui/icons/Edit'; import { QUERY_TOOL_EVENTS } from '../QueryToolConstants'; @@ -21,51 +21,12 @@ import MapIcon from '@material-ui/icons/Map'; import { QueryToolEventsContext } from '../QueryToolComponent'; import PropTypes from 'prop-types'; import gettext from 'sources/gettext'; +import PgReactDataGrid from '../../../../../../static/js/components/PgReactDataGrid'; export const ROWNUM_KEY = '$_pgadmin_rownum_key_$'; export const GRID_ROW_SELECT_KEY = '$_pgadmin_gridrowselect_key_$'; const useStyles = makeStyles((theme)=>({ - root: { - height: '100%', - color: theme.palette.text.primary, - backgroundColor: theme.otherVars.qtDatagridBg, - fontSize: '12px', - border: 'none', - '--rdg-selection-color': theme.palette.primary.main, - '& .rdg-cell': { - ...theme.mixins.panelBorder.right, - ...theme.mixins.panelBorder.bottom, - fontWeight: 'abc', - '&[aria-colindex="1"]': { - padding: 0, - }, - '&[aria-selected=true]:not([role="columnheader"]):not([aria-colindex="1"])': { - outlineWidth: '1px', - outlineOffset: '-1px', - backgroundColor: theme.palette.primary.light, - color: theme.otherVars.qtDatagridSelectFg, - } - }, - '& .rdg-header-row .rdg-cell': { - padding: 0, - }, - '& .rdg-header-row': { - backgroundColor: theme.palette.background.default, - fontWeight: 'normal', - }, - '& .rdg-row': { - backgroundColor: theme.palette.background.default, - '&[aria-selected=true]': { - backgroundColor: theme.palette.primary.light, - color: theme.otherVars.qtDatagridSelectFg, - '& .rdg-cell:nth-child(1)': { - backgroundColor: theme.palette.primary.main, - color: theme.palette.primary.contrastText, - } - }, - } - }, columnHeader: { padding: '3px 6px', height: '100%', @@ -408,11 +369,10 @@ export default function QueryToolDataGrid({columns, rows, totalRowCount, dataCha return ( - ({ sql: { @@ -294,7 +295,7 @@ export default function Query() { eventBus.registerListener(QUERY_TOOL_EVENTS.LOAD_FILE, (fileName)=>{ queryToolCtx.api.post(url_for('sqleditor.load_file'), { 'file_name': decodeURI(fileName), - }).then((res)=>{ + }, {transformResponse: [(data) => { return data; }]}).then((res)=>{ editor.current.setValue(res.data); //Check the file content for Trojan Source checkTrojanSource(res.data); @@ -302,7 +303,7 @@ export default function Query() { eventBus.fireEvent(QUERY_TOOL_EVENTS.LOAD_FILE_DONE, fileName, true); }).catch((err)=>{ eventBus.fireEvent(QUERY_TOOL_EVENTS.LOAD_FILE_DONE, null, false); - eventBus.fireEvent(QUERY_TOOL_EVENTS.HANDLE_API_ERROR, err); + Notifier.error(parseApiError(err)); }); }); diff --git a/web/pgadmin/tools/storage_manager/__init__.py b/web/pgadmin/tools/storage_manager/__init__.py deleted file mode 100644 index 7850f6f95..000000000 --- a/web/pgadmin/tools/storage_manager/__init__.py +++ /dev/null @@ -1,51 +0,0 @@ -########################################################################## -# -# pgAdmin 4 - PostgreSQL Tools -# -# Copyright (C) 2013 - 2022, The pgAdmin Development Team -# This software is released under the PostgreSQL Licence -# -########################################################################## - -"""A blueprint module implementing the storage manager functionality""" - -from flask import url_for, Response, render_template -from flask_babel import gettext as _ -from flask_security import login_required - -from pgadmin.utils import PgAdminModule -from pgadmin.utils.ajax import bad_request -from pgadmin.utils.constants import MIMETYPE_APP_JS - -MODULE_NAME = 'storage_manager' - - -class StorageManagerModule(PgAdminModule): - """ - class StorageManagerModule(PgAdminModule) - - A module class for manipulating file operation which is derived from - PgAdminModule. - """ - - LABEL = _('Storage Manager') - - -blueprint = StorageManagerModule(MODULE_NAME, __name__) - - -@blueprint.route("/") -@login_required -def index(): - return bad_request(errormsg=_("This URL cannot be called directly.")) - - -@blueprint.route("/js/storage_manager.js") -@login_required -def script(): - """render the import/export javascript file""" - return Response( - response=render_template("storage_manager/js/storage_manager.js", _=_), - status=200, - mimetype=MIMETYPE_APP_JS - ) diff --git a/web/pgadmin/tools/storage_manager/static/js/storage_manager.js b/web/pgadmin/tools/storage_manager/static/js/storage_manager.js deleted file mode 100644 index cba26d288..000000000 --- a/web/pgadmin/tools/storage_manager/static/js/storage_manager.js +++ /dev/null @@ -1,93 +0,0 @@ -///////////////////////////////////////////////////////////// -// -// pgAdmin 4 - PostgreSQL Tools -// -// Copyright (C) 2013 - 2022, The pgAdmin Development Team -// This software is released under the PostgreSQL Licence -// -////////////////////////////////////////////////////////////// -import { set_last_traversed_dir, getTransId } from '../../../../misc/file_manager/static/js/helpers'; - -define([ - 'sources/gettext', 'sources/url_for', 'jquery', 'underscore', 'pgadmin.alertifyjs', - 'sources/pgadmin', 'pgadmin.browser', 'sources/csrf', 'pgadmin.file_manager', -], function ( - gettext, url_for, $, _, alertify, pgAdmin, pgBrowser, csrfToken -) { - - pgAdmin = pgAdmin || window.pgAdmin || {}; - var isServerMode = (function() { return pgAdmin.server_mode == 'True'; })(); - - var pgTools = pgAdmin.Tools = pgAdmin.Tools || {}; - - if(!isServerMode) { - return; - } - - // Return back, this has been called more than once - if (pgAdmin.Tools.storage_manager) - return pgAdmin.Tools.storage_manager; - - pgTools.storage_manager = { - init: function () { - // We do not want to initialize the module multiple times. - if (this.initialized) - return; - - this.initialized = true; - csrfToken.setPGCSRFToken(pgAdmin.csrf_token_header, pgAdmin.csrf_token); - - var storage_manager = this.callback_storage_manager.bind(this); - - pgBrowser.Events.on( - 'pgadmin:tools:storage_manager', storage_manager - ); - - // Define the nodes on which the menus to be appear - var menus = [{ - name: 'storage_manager', - module: this, - applies: ['tools'], - callback: 'callback_storage_manager', - priority: 11, - label: gettext('Storage Manager...'), - enable: true, - }]; - - pgBrowser.add_menus(menus); - }, - - /* - Open the dialog for the storage functionality - */ - callback_storage_manager: function (path) { - - var params = { - supported_types: ['sql', 'csv', 'json', '*'], - dialog_type: 'storage_dialog', - dialog_title: 'Storage Manager', - btn_primary: undefined, - }; - - if (!_.isUndefined(path) && !_.isNull(path) && !_.isEmpty(path)) { - - var transId = getTransId(JSON.stringify(params)); - var t_res; - if (transId.readyState == 4) { - t_res = JSON.parse(transId.responseText); - } - var trans_id = _.isUndefined(t_res) ? 0 : t_res.data.fileTransId; - - set_last_traversed_dir({'path': path}, trans_id); - pgAdmin.FileManager.init(); - pgAdmin.FileManager.show_dialog(params); - } - else { - pgAdmin.FileManager.init(); - pgAdmin.FileManager.show_dialog(params); - } - }, - }; - - return pgAdmin.Tools.storage_manager; -}); diff --git a/web/regression/javascript/SchemaView/SchemaView.spec.js b/web/regression/javascript/SchemaView/SchemaView.spec.js index 36757abba..aea2c1d40 100644 --- a/web/regression/javascript/SchemaView/SchemaView.spec.js +++ b/web/regression/javascript/SchemaView/SchemaView.spec.js @@ -15,7 +15,6 @@ import {TestSchema, TestSchemaAllTypes} from './TestSchema.ui'; import pgAdmin from 'sources/pgadmin'; import {messages} from '../fake_messages'; import SchemaView from '../../../pgadmin/static/js/SchemaView'; -import * as legacyConnector from 'sources/helpers/legacyConnector'; import Notify from '../../../pgadmin/static/js/helpers/Notifier'; import Theme from '../../../pgadmin/static/js/Theme'; @@ -191,18 +190,17 @@ describe('SchemaView', ()=>{ simulateValidData(); /* Press OK */ - let confirmSpy = spyOn(legacyConnector, 'confirmDeleteRow').and.callFake((yesFn)=>{ - yesFn(); - }); + let confirmSpy = spyOn(Notify, 'confirm').and.callThrough(); ctrl.find('DataGridView').find('PgIconButton[data-test="delete-row"]').at(0).find('button').simulate('click'); - expect(confirmSpy.calls.argsFor(0)[2]).toBe('Custom delete title'); - expect(confirmSpy.calls.argsFor(0)[3]).toBe('Custom delete message'); + confirmSpy.calls.argsFor(0)[2](); + expect(confirmSpy.calls.argsFor(0)[0]).toBe('Custom delete title'); + expect(confirmSpy.calls.argsFor(0)[1]).toBe('Custom delete message'); /* Press Cancel */ - spyOn(legacyConnector, 'confirmDeleteRow').and.callFake((yesFn, cancelFn)=>{ - cancelFn(); - }); + confirmSpy.calls.reset(); ctrl.find('DataGridView').find('PgIconButton[data-test="delete-row"]').at(0).find('button').simulate('click'); + confirmSpy.calls.argsFor(0)[3](); + setTimeout(()=>{ ctrlUpdate(done); }, 0); @@ -297,7 +295,7 @@ describe('SchemaView', ()=>{ }, 0); }); - let onRestAction = (done, data)=> { + let onResetAction = (done, data)=> { ctrl.update(); expect(ctrl.find('DefaultButton[data-test="Reset"]').prop('disabled')).toBeTrue(); expect(ctrl.find('PrimaryButton[data-test="Save"]').prop('disabled')).toBeTrue(); @@ -316,7 +314,7 @@ describe('SchemaView', ()=>{ /* Press OK */ confirmSpy.calls.argsFor(0)[2](); setTimeout(()=>{ - onRestAction(done, { id: undefined, field1: null, field2: null, fieldcoll: null }); + onResetAction(done, { id: undefined, field1: null, field2: null, fieldcoll: null }); }, 0); }, 0); }); @@ -390,7 +388,9 @@ describe('SchemaView', ()=>{ ctrl.find('MappedCellControl[id="field5"]').at(2).find('input').simulate('change', {target: {value: 'rval53'}}); /* Remove the 1st row */ + let confirmSpy = spyOn(Notify, 'confirm').and.callThrough(); ctrl.find('DataTableRow').find('PgIconButton[data-test="delete-row"]').at(0).find('button').simulate('click'); + confirmSpy.calls.argsFor(0)[2](); /* Edit the 2nd row which is first now*/ ctrl.find('MappedCellControl[id="field5"]').at(0).find('input').simulate('change', {target: {value: 'rvalnew'}}); @@ -403,11 +403,6 @@ describe('SchemaView', ()=>{ mode: 'edit', } }); - - /* Press OK */ - spyOn(legacyConnector, 'confirmDeleteRow').and.callFake((yesFn)=>{ - yesFn(); - }); }); it('init', (done)=>{ setTimeout(()=>{ @@ -463,9 +458,9 @@ describe('SchemaView', ()=>{ let confirmSpy = spyOn(Notify, 'confirm').and.callThrough(); ctrl.find('DefaultButton[data-test="Reset"]').simulate('click'); /* Press OK */ - confirmSpy.calls.argsFor(0)[2](); + confirmSpy.calls.mostRecent().args[2](); setTimeout(()=>{ - onRestAction(done, {}); + onResetAction(done, {}); }, 0); }, 0); }); diff --git a/web/regression/javascript/components/FormComponents.spec.js b/web/regression/javascript/components/FormComponents.spec.js index be70cb272..635e09756 100644 --- a/web/regression/javascript/components/FormComponents.spec.js +++ b/web/regression/javascript/components/FormComponents.spec.js @@ -28,10 +28,10 @@ import {FormInputText, FormInputFileSelect, FormInputSQL, FormInputColor, FormFooterMessage, MESSAGE_TYPE} from '../../../pgadmin/static/js/components/FormComponents'; -import * as legacyConnector from '../../../pgadmin/static/js/helpers/legacyConnector'; import CodeMirror from '../../../pgadmin/static/js/components/CodeMirror'; import { ToggleButton } from '@material-ui/lab'; import { DefaultButton, PrimaryButton } from '../../../pgadmin/static/js/components/Buttons'; +import * as showFileManager from '../../../pgadmin/static/js/helpers/showFileManager'; /* MUI Components need to be wrapped in Theme for theme vars */ describe('FormComponents', ()=>{ @@ -118,7 +118,7 @@ describe('FormComponents', ()=>{ let ThemedFormInputFileSelect = withTheme(FormInputFileSelect), ctrl; beforeEach(()=>{ - spyOn(legacyConnector, 'showFileDialog').and.callFake((controlProps, onFileSelect)=>{ + spyOn(showFileManager, 'showFileManager').and.callFake((controlProps, onFileSelect)=>{ onFileSelect('selected/file'); }); ctrl = mount( diff --git a/web/regression/javascript/erd/ui_components/body_widget_spec.js b/web/regression/javascript/erd/ui_components/body_widget_spec.js index 74afced32..04f9fa13c 100644 --- a/web/regression/javascript/erd/ui_components/body_widget_spec.js +++ b/web/regression/javascript/erd/ui_components/body_widget_spec.js @@ -38,12 +38,11 @@ let pgAdmin = { app_version_int: 1234, }, }, - FileManager: { - init: jasmine.createSpy(), - show_dialog: jasmine.createSpy(), - }, Tools: { SQLEditor: {}, + FileManager: { + show: jasmine.createSpy(), + }, } }; @@ -360,7 +359,7 @@ describe('ERD BodyWidget', ()=>{ it('onLoadDiagram', ()=>{ bodyInstance.onLoadDiagram(); - expect(pgAdmin.FileManager.show_dialog).toHaveBeenCalled(); + expect(pgAdmin.Tools.FileManager.show).toHaveBeenCalled(); }); it('openFile', (done)=>{ @@ -389,9 +388,10 @@ describe('ERD BodyWidget', ()=>{ done(); }); + pgAdmin.Tools.FileManager.show.calls.reset(); bodyInstance.onSaveDiagram(true); - expect(pgAdmin.FileManager.show_dialog).toHaveBeenCalledWith({ - 'supported_types': ['pgerd'], + expect(pgAdmin.Tools.FileManager.show.calls.argsFor(0)[0]).toEqual({ + 'supported_types': ['*','pgerd'], 'dialog_type': 'create_file', 'dialog_title': 'Save File', 'btn_primary': 'Save', diff --git a/web/regression/javascript/fake_endpoints.js b/web/regression/javascript/fake_endpoints.js index fd5ec175f..79686fe55 100644 --- a/web/regression/javascript/fake_endpoints.js +++ b/web/regression/javascript/fake_endpoints.js @@ -28,5 +28,13 @@ define(function () { 'erd.sql': '/erd/sql////', 'erd.prequisite': '/erd/prequisite////', 'erd.tables': '/erd/tables////', + 'file_manager.init': '/file_manager/init', + 'file_manager.filemanager': '/file_manager/init', + 'file_manager.index': '/file_manager/', + 'file_manager.delete_trans_id': '/file_manager/delete_trans_id/', + 'file_manager.save_last_dir': '/file_manager/save_last_dir/', + 'file_manager.save_file_dialog_view': '/file_manager/save_file_dialog_view/', + 'file_manager.save_show_hidden_file_option': '/file_manager/save_show_hidden_file_option/', + 'settings.save_file_format_setting': '/settings/save_file_format_setting/', }; }); diff --git a/web/regression/javascript/file_manager/FileManager.spec.js b/web/regression/javascript/file_manager/FileManager.spec.js new file mode 100644 index 000000000..c41136796 --- /dev/null +++ b/web/regression/javascript/file_manager/FileManager.spec.js @@ -0,0 +1,324 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2022, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import jasmineEnzyme from 'jasmine-enzyme'; +import React from 'react'; +import '../helper/enzyme.helper'; +import { createMount } from '@material-ui/core/test-utils'; +import Theme from '../../../pgadmin/static/js/Theme'; +import FileManager, { FileManagerUtils, getComparator } from '../../../pgadmin/misc/file_manager/static/js/components/FileManager'; +import MockAdapter from 'axios-mock-adapter'; +import axios from 'axios/index'; +import getApiInstance from '../../../pgadmin/static/js/api_instance'; +import * as pgUtils from '../../../pgadmin/static/js/utils'; + +const files = [ + { + 'Filename': 'file1.sql', + 'Path': '/home/file1', + 'file_type': 'sql', + 'Protected': 0, + 'Properties': { + 'Date Created': 'Fri Oct 22 16:59:24 2021', + 'Date Modified': 'Tue Oct 12 14:08:00 2021', + 'Size': '1.4 MB' + } + }, + { + 'Filename': 'folder1', + 'Path': '/home/folder1', + 'file_type': 'dir', + 'Protected': 0, + 'Properties': { + 'Date Created': 'Fri Oct 22 16:59:24 2021', + 'Date Modified': 'Tue Oct 12 14:08:00 2021', + 'Size': '1.4 MB' + } + } +]; + +const transId = 140391; +const configData = { + 'transId': transId, + 'options': { + 'culture': 'en', + 'lang': 'py', + 'defaultViewMode':'list', + 'autoload': true, + 'showFullPath': false, + 'dialog_type': 'select_folder', + 'show_hidden_files': false, + 'fileRoot': '/home/current', + 'capabilities': [ + 'select_folder', 'select_file', 'download', + 'rename', 'delete', 'upload', 'create' + ], + 'allowed_file_types': [ + '*', + 'sql', + 'backup' + ], + 'platform_type': 'darwin', + 'show_volumes': true, + 'homedir': '/home/', + 'last_selected_format': '*' + }, + 'security': { + 'uploadPolicy': '', + 'uploadRestrictions': [ + '*', + 'sql', + 'backup' + ] + }, + 'upload': { + 'multiple': true, + 'number': 20, + 'fileSizeLimit': 50, + 'imagesOnly': false + } +}; + +const params={ + dialog_type: 'select_file', +}; + +describe('FileManger', ()=>{ + let mount; + let networkMock; + + /* Use createMount so that material ui components gets the required context */ + /* https://material-ui.com/guides/testing/#api */ + beforeAll(()=>{ + mount = createMount(); + networkMock = new MockAdapter(axios); + networkMock.onPost('/file_manager/init').reply(200, {'data': configData}); + networkMock.onPost(`/file_manager/filemanager/${transId}/`).reply(200, {data: {result: files}}); + networkMock.onDelete(`/file_manager/delete_trans_id/${transId}`).reply(200, {}); + }); + + afterAll(() => { + mount.cleanUp(); + networkMock.restore(); + }); + + beforeEach(()=>{ + jasmineEnzyme(); + }); + + describe('FileManger', ()=>{ + let closeModal=jasmine.createSpy('closeModal'), + onOK=jasmine.createSpy('onOK'), + onCancel=jasmine.createSpy('onCancel'), + ctrlMount = (props)=>{ + return mount( + + ); + }; + + it('init', (done)=>{ + let ctrl = ctrlMount({}); + setTimeout(()=>{ + ctrl.update(); + expect(ctrl.find('ListView').length).toBe(1); + expect(ctrl.find('GridView').length).toBe(0); + expect(ctrl.find('InputText[data-label="file-path"]').prop('value')).toBe('/home/current'); + ctrl?.unmount(); + let config = {...configData}; + config.options.defaultViewMode = 'grid'; + networkMock.onPost('/file_manager/init').reply(200, {'data': config}); + ctrl = ctrlMount({}); + setTimeout(()=>{ + ctrl.update(); + expect(ctrl.find('ListView').length).toBe(0); + expect(ctrl.find('GridView').length).toBe(1); + ctrl?.unmount(); + done(); + }, 0); + }, 500); + }); + + describe('getComparator', ()=>{ + it('Filename', ()=>{ + expect(getComparator({columnKey: 'Filename', direction: 'ASC'})({Filename:'a'}, {Filename:'b'})).toBe(-1); + expect(getComparator({columnKey: 'Filename', direction: 'DESC'})({Filename:'a'}, {Filename:'b'})).toBe(1); + expect(getComparator({columnKey: 'Filename', direction: 'ASC'})({Filename:'a'}, {Filename:'A'})).toBe(-1); + }); + + it('Properties.DateModified', ()=>{ + expect(getComparator({columnKey: 'Properties.DateModified', direction: 'ASC'})( + {Properties:{'Date Modified':'Tue Feb 25 11:36:28 2020'}}, {Properties:{'Date Modified':'Tue Feb 26 11:36:28 2020'}}) + ).toBe(-1); + expect(getComparator({columnKey: 'Properties.DateModified', direction: 'DESC'})( + {Properties:{'Date Modified':'Tue Feb 25 11:36:28 2020'}}, {Properties:{'Date Modified':'Tue Feb 26 11:36:28 2020'}}) + ).toBe(1); + expect(getComparator({columnKey: 'Properties.DateModified', direction: 'ASC'})( + {Properties:{'Date Modified':'Tue Feb 25 11:36:28 2020'}}, {Properties:{'Date Modified':'Tue Feb 25 11:36:28 2020'}}) + ).toBe(0); + }); + + it('Properties.Size', ()=>{ + expect(getComparator({columnKey: 'Properties.Size', direction: 'ASC'})( + {Properties:{'Size':'1 KB'}}, {Properties:{'Size':'1 MB'}}) + ).toBe(-1); + expect(getComparator({columnKey: 'Properties.Size', direction: 'DESC'})( + {Properties:{'Size':'1 MB'}}, {Properties:{'Size':'1 GB'}}) + ).toBe(1); + expect(getComparator({columnKey: 'Properties.Size', direction: 'ASC'})( + {Properties:{'Size':'1 MB'}}, {Properties:{'Size':'1 MB'}}) + ).toBe(0); + }); + }); + }); +}); + +describe('FileManagerUtils', ()=>{ + let api, fmObj, networkMock; + beforeEach(()=>{ + networkMock = new MockAdapter(axios); + networkMock.onDelete(`/file_manager/delete_trans_id/${transId}`).reply(200, {}); + networkMock.onPost(`/file_manager/filemanager/${transId}/`).reply((config)=>{ + let retVal = {}; + let apiData = JSON.parse(config.data); + let headers = {}; + if(apiData.mode == 'addfolder') { + retVal = {data: {result: { + Name: apiData.name, + Path: '/home/'+apiData.name, + 'Date Modified': 'Tue Feb 25 11:36:28 2020', + }}}; + } else if(apiData.mode == 'rename') { + retVal = {data: {result: { + 'New Path': '/home/'+apiData.new, + 'New Name': apiData.new, + }}}; + } else if(apiData.mode == 'download') { + retVal = 'blobdata'; + headers = {filename: 'newfile1'}; + } else if(apiData.mode == 'is_file_exist') { + retVal = {data: {result: {Code: 1}}}; + } + return [200, retVal, headers]; + }); + + api = getApiInstance(); + fmObj = new FileManagerUtils(api, params); + fmObj.config = configData; + }); + + afterEach(()=>{ + networkMock.restore(); + }); + + it('showHiddenFiles', ()=>{ + expect(fmObj.showHiddenFiles).toBe(false); + networkMock.onPut(`/file_manager/save_show_hidden_file_option/${transId}`).reply(200, {}); + fmObj.showHiddenFiles = true; + expect(fmObj.config.options?.show_hidden_files).toBe(true); + }); + + it('setLastVisitedDir', async ()=>{ + let calledPath = null; + networkMock.onPost(`/file_manager/save_last_dir/${transId}`).reply((config)=>{ + calledPath = JSON.parse(config.data).path; + return [200, {}]; + }); + await fmObj.setLastVisitedDir('/home/xyz'); + expect(calledPath).toBe('/home/xyz'); + }); + + it('setDialogView', async ()=>{ + networkMock.onPost(`/file_manager/save_file_dialog_view/${transId}`).reply(200, {}); + await fmObj.setDialogView('grid'); + expect(fmObj.config.options.defaultViewMode).toBe('grid'); + }); + + it('setFileType', async ()=>{ + networkMock.onPost('/settings/save_file_format_setting/').reply(200, {}); + await fmObj.setFileType('pgerd'); + expect(fmObj.config.options.last_selected_format).toBe('pgerd'); + }); + + it('join', ()=>{ + expect(fmObj.join('/dir1/dir2', 'file1')).toBe('/dir1/dir2/file1'); + expect(fmObj.join('/dir1/dir2/', 'file1')).toBe('/dir1/dir2/file1'); + }); + + it('addFolder', async ()=>{ + let res = await fmObj.addFolder({Filename: 'newfolder'}); + expect(res).toEqual({ + Filename: 'newfolder', + Path: '/home/newfolder', + file_type: 'dir', + Properties: { + 'Date Modified': 'Tue Feb 25 11:36:28 2020', + } + }); + }); + + it('rename', async ()=>{ + let row = {Filename: 'newfolder1', Path: '/home/newfolder'}; + let res = await fmObj.renameItem(row); + expect(res).toEqual({ + Filename: 'newfolder1', + Path: '/home/newfolder1', + }); + }); + + it('deleteItem', async ()=>{ + let row = {Filename: 'newfolder', Path: '/home/newfolder'}; + let path = await fmObj.deleteItem(row); + expect(path).toBe('/home/newfolder'); + + path = await fmObj.deleteItem(row, 'file1'); + expect(path).toBe('/home/newfolder/file1'); + }); + + it('checkPermission', async ()=>{ + networkMock.reset(); + networkMock.onPost(`/file_manager/filemanager/${transId}/`).reply(200, { + data: { + result: { + Code: 1, + } + } + }); + let res = await fmObj.checkPermission('/home/newfolder'); + expect(res).toEqual(null); + + networkMock.onPost(`/file_manager/filemanager/${transId}/`).reply(200, { + data: { + result: { + Code: 0, + Error: 'file error' + } + } + }); + res = await fmObj.checkPermission('/home/newfolder'); + expect(res).toEqual('file error'); + }); + + it('isFileExists', async ()=>{ + let res = await fmObj.isFileExists('/home/newfolder', 'newfile1'); + expect(res).toBe(true); + }); + + it('downloadFile', async ()=>{ + spyOn(pgUtils, 'downloadBlob'); + let row = {Filename: 'newfile1', Path: '/home/newfile1'}; + await fmObj.downloadFile(row); + expect(pgUtils.downloadBlob).toHaveBeenCalledWith('blobdata', 'newfile1'); + }); +}); diff --git a/web/regression/javascript/file_manager/GridView.spec.js b/web/regression/javascript/file_manager/GridView.spec.js new file mode 100644 index 000000000..b2bb7483e --- /dev/null +++ b/web/regression/javascript/file_manager/GridView.spec.js @@ -0,0 +1,62 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2022, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import jasmineEnzyme from 'jasmine-enzyme'; +import React from 'react'; +import '../helper/enzyme.helper'; +import { createMount } from '@material-ui/core/test-utils'; +import Theme from '../../../pgadmin/static/js/Theme'; +import { ItemView } from '../../../pgadmin/misc/file_manager/static/js/components/GridView'; + +describe('GridView', ()=>{ + let mount; + + /* Use createMount so that material ui components gets the required context */ + /* https://material-ui.com/guides/testing/#api */ + beforeAll(()=>{ + mount = createMount(); + }); + + afterAll(() => { + mount.cleanUp(); + }); + + beforeEach(()=>{ + jasmineEnzyme(); + }); + + describe('ItemView', ()=>{ + let row = {'Filename': 'test.sql', 'Size': '1KB', 'file_type': 'dir'}, + ctrlMount = (props)=>{ + return mount( + + ); + }; + + it('keydown Escape', (done)=>{ + const onEditComplete = jasmine.createSpy('onEditComplete'); + let ctrl = ctrlMount({ + onEditComplete: onEditComplete, + }); + setTimeout(()=>{ + ctrl.update(); + ctrl.find('div[data-test="filename-div"]').simulate('keydown', { code: 'Escape'}); + setTimeout(()=>{ + expect(onEditComplete).toHaveBeenCalled(); + done(); + }); + }, 0); + }); + }); +}); diff --git a/web/regression/javascript/file_manager/ListView.spec.js b/web/regression/javascript/file_manager/ListView.spec.js new file mode 100644 index 000000000..8036579ae --- /dev/null +++ b/web/regression/javascript/file_manager/ListView.spec.js @@ -0,0 +1,110 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2022, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import jasmineEnzyme from 'jasmine-enzyme'; +import React from 'react'; +import '../helper/enzyme.helper'; +import { createMount } from '@material-ui/core/test-utils'; +import Theme from '../../../pgadmin/static/js/Theme'; +import { CustomRow, FileNameEditor, GridContextUtils } from '../../../pgadmin/misc/file_manager/static/js/components/ListView'; + +describe('ListView', ()=>{ + let mount; + + /* Use createMount so that material ui components gets the required context */ + /* https://material-ui.com/guides/testing/#api */ + beforeAll(()=>{ + mount = createMount(); + }); + + afterAll(() => { + mount.cleanUp(); + }); + + beforeEach(()=>{ + jasmineEnzyme(); + }); + + describe('FileNameEditor', ()=>{ + let row = {'Filename': 'test.sql', 'Size': '1KB'}, + column = { + key: 'Filename' + }, + ctrlMount = (props)=>{ + return mount( + + ); + }; + + it('init', (done)=>{ + let ctrl = ctrlMount({ + onRowChange: ()=>{/* test func */}, + onClose: ()=>{/* test func */}, + }); + setTimeout(()=>{ + ctrl.update(); + expect(ctrl.find('input').props()).toEqual(jasmine.objectContaining({value: 'test.sql'})); + done(); + }, 0); + }); + + it('keydown Tab', (done)=>{ + let onCloseSpy = jasmine.createSpy('onClose'); + let ctrl = ctrlMount({ + onRowChange: ()=>{/* test func */}, + onClose: onCloseSpy, + }); + setTimeout(()=>{ + ctrl.update(); + expect(ctrl.find('input').props()).toEqual(jasmine.objectContaining({value: 'test.sql'})); + ctrl.find('input').simulate('keydown', { code: 'Tab'}); + setTimeout(()=>{ + expect(onCloseSpy).toHaveBeenCalled(); + done(); + }); + }, 0); + }); + }); + + describe('CustomRow', ()=>{ + let row = {'Filename': 'test.sql', 'Size': '1KB'}, + ctrlMount = (onItemSelect, onItemEnter)=>{ + return mount( + + + + ); + }; + + it('init', (done)=>{ + let onItemSelect = jasmine.createSpy('onItemSelect'); + let onItemEnter = jasmine.createSpy('onItemEnter'); + let ctrl = ctrlMount(onItemSelect, onItemEnter); + setTimeout(()=>{ + ctrl.update(); + ctrl.find('div[data-test="test-div"]').simulate('keydown', { code: 'Enter'}); + setTimeout(()=>{ + ctrl.update(); + expect(onItemEnter).toHaveBeenCalled(); + ctrl?.unmount(); + done(); + }, 0); + }, 0); + }); + }); +}); diff --git a/web/regression/javascript/file_manager/Uploader.spec.js b/web/regression/javascript/file_manager/Uploader.spec.js new file mode 100644 index 000000000..8c094c237 --- /dev/null +++ b/web/regression/javascript/file_manager/Uploader.spec.js @@ -0,0 +1,233 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2022, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import jasmineEnzyme from 'jasmine-enzyme'; +import React from 'react'; +import '../helper/enzyme.helper'; +import { createMount } from '@material-ui/core/test-utils'; +import Theme from '../../../pgadmin/static/js/Theme'; +import Uploader, { filesReducer, getFileSize, UploadedFile } from '../../../pgadmin/misc/file_manager/static/js/components/Uploader'; +import { MESSAGE_TYPE } from '../../../pgadmin/static/js/components/FormComponents'; + +describe('GridView', ()=>{ + let mount; + + /* Use createMount so that material ui components gets the required context */ + /* https://material-ui.com/guides/testing/#api */ + beforeAll(()=>{ + mount = createMount(); + }); + + afterAll(() => { + mount.cleanUp(); + }); + + beforeEach(()=>{ + jasmineEnzyme(); + }); + + describe('Uploader', ()=>{ + let fmUtilsObj = jasmine.createSpyObj('fmUtilsObj', ['uploadItem', 'deleteItem'], ['currPath']); + let onClose = jasmine.createSpy('onClose'); + let ctrlMount = (props)=>{ + return mount( + + ); + }; + + it('init', (done)=>{ + let ctrl = ctrlMount(); + setTimeout(()=>{ + ctrl.update(); + done(); + }, 0); + }); + + describe('filesReducer', ()=>{ + let state; + + beforeEach(()=>{ + state = [ + { + id: 1, + file: 'file1', + progress: 0, + started: false, + failed: false, + done: false, + } + ]; + }); + + it('add', ()=>{ + let newState = filesReducer(state, { + type: 'add', + files: ['new1'], + }); + expect(newState.length).toBe(2); + expect(newState[0]).toEqual(jasmine.objectContaining({ + file: 'new1', + progress: 0, + started: false, + failed: false, + done: false, + })); + }); + + + it('started', ()=>{ + let newState = filesReducer(state, { + type: 'started', + id: 1, + }); + expect(newState[0]).toEqual(jasmine.objectContaining({ + file: 'file1', + progress: 0, + started: true, + failed: false, + done: false, + })); + }); + + it('started', ()=>{ + let newState = filesReducer(state, { + type: 'progress', + id: 1, + value: 14, + }); + expect(newState[0]).toEqual(jasmine.objectContaining({ + file: 'file1', + progress: 14, + started: false, + failed: false, + done: false, + })); + }); + + it('failed', ()=>{ + let newState = filesReducer(state, { + type: 'failed', + id: 1, + }); + expect(newState[0]).toEqual(jasmine.objectContaining({ + file: 'file1', + progress: 0, + started: false, + failed: true, + done: false, + })); + }); + + it('done', ()=>{ + let newState = filesReducer(state, { + type: 'done', + id: 1, + }); + expect(newState[0]).toEqual(jasmine.objectContaining({ + file: 'file1', + progress: 0, + started: false, + failed: false, + done: true, + })); + }); + + it('remove', ()=>{ + let newState = filesReducer(state, { + type: 'remove', + id: 1, + }); + expect(newState.length).toBe(0); + }); + }); + + it('getFileSize', ()=>{ + expect(getFileSize(1024)).toBe('1 KB'); + }); + + describe('UploadedFile', ()=>{ + let upCtrlMount = (props)=>{ + return mount( + {/*dummy*/}} + onClose={onClose} + {...props} + /> + ); + }; + + it('uploading', (done)=>{ + let ctrl = upCtrlMount({upfile: { + file: { + name: 'file1', + size: '1KB', + }, + done: false, + failed: false, + progress: 14, + }}); + + setTimeout(()=>{ + ctrl.update(); + expect(ctrl.find('FormFooterMessage').props()).toEqual(jasmine.objectContaining({ + type: MESSAGE_TYPE.INFO, + message: 'Uploading... 14%', + })); + done(); + }, 0); + }); + + it('done', (done)=>{ + let ctrl = upCtrlMount({upfile: { + file: { + name: 'file1', + size: '1KB', + }, + done: true, + failed: false, + progress: 14, + }}); + + setTimeout(()=>{ + ctrl.update(); + expect(ctrl.find('FormFooterMessage').props()).toEqual(jasmine.objectContaining({ + type: MESSAGE_TYPE.SUCCESS, + message: 'Uploaded!', + })); + done(); + }, 0); + }); + + it('failed', (done)=>{ + let ctrl = upCtrlMount({upfile: { + file: { + name: 'file1', + size: '1KB', + }, + done: false, + failed: true, + progress: 14, + }}); + + setTimeout(()=>{ + ctrl.update(); + expect(ctrl.find('FormFooterMessage').props()).toEqual(jasmine.objectContaining({ + type: MESSAGE_TYPE.ERROR, + message: 'Failed!', + })); + done(); + }, 0); + }); + }); + }); +}); diff --git a/web/regression/javascript/file_manager/file_manager_specs.js b/web/regression/javascript/file_manager/file_manager_specs.js deleted file mode 100644 index fd860817e..000000000 --- a/web/regression/javascript/file_manager/file_manager_specs.js +++ /dev/null @@ -1,128 +0,0 @@ -///////////////////////////////////////////////////////////// -// -// pgAdmin 4 - PostgreSQL Tools -// -// Copyright (C) 2013 - 2022, The pgAdmin Development Team -// This software is released under the PostgreSQL Licence -// -////////////////////////////////////////////////////////////// - -import pgAdmin from 'sources/pgadmin'; -import Alertify from 'pgadmin.alertifyjs'; -import '../../../pgadmin/misc/file_manager/static/js/file_manager'; -import '../../../pgadmin/misc/file_manager/static/js/select_dialogue.js'; - - -describe('fileSelectDialog', function () { - - let params; - let calcWidth = (passed_width)=>{ - let iw = window.innerWidth; - if (iw > passed_width){ - return passed_width; - } else { - if (iw > pgAdmin.Browser.stdW.lg) - return pgAdmin.Browser.stdW.lg; - else if (iw > pgAdmin.Browser.stdW.md) - return pgAdmin.Browser.stdW.md; - else if (iw > pgAdmin.Browser.stdW.sm) - return pgAdmin.Browser.stdW.sm; - else - // if available screen resolution is still - // less then return the width value as it - return iw; - } - }; - - let calcHeight = (passed_height)=>{ - // We are excluding sm as it is too small for dialog - let ih = window.innerHeight; - if (ih > passed_height){ - return passed_height; - }else{ - if (ih > pgAdmin.Browser.stdH.lg) - return pgAdmin.Browser.stdH.lg; - else if (ih > pgAdmin.Browser.stdH.md) - return pgAdmin.Browser.stdH.md; - else - // if available screen resolution is still - // less then return the height value as it - return ih; - } - }; - - beforeAll(() => { - pgAdmin.Browser = { - stdW: { - sm: 500, - md: 700, - lg: 900, - default: 500, - calc: (passed_width) => { - calcWidth(passed_width); - }, - }, - stdH: { - sm: 200, - md: 400, - lg: 550, - default: 550, - calc: (passed_height) => { - calcHeight(passed_height); - }, - }, - }; - }); - - describe('When dialog is called for', () => { - it('Select file', function() { - params = { - 'dialog_title': 'Select file', - 'dialog_type': 'select_file', - }; - - spyOn(Alertify, 'fileSelectionDlg').and.callFake(function() { - this.resizeTo = function() {/*This is intentional (SonarQube)*/}; - return this; - }); - - pgAdmin.FileManager.show_dialog(params); - - expect(Alertify.fileSelectionDlg).toHaveBeenCalled(); - }); - - it('create file', function() { - params = { - 'dialog_title': 'Create file', - 'dialog_type': 'create_file', - }; - - spyOn(Alertify, 'createModeDlg').and.callFake(function() { - this.resizeTo = function() {/*This is intentional (SonarQube)*/}; - return this; - }); - - pgAdmin.FileManager.show_dialog(params); - - expect(Alertify.createModeDlg).toHaveBeenCalled(); - }); - }); - - describe('When dialog is called for storage file', () => { - it('Storage file dialog', function() { - params = { - 'dialog_title': 'Storage Manager', - 'dialog_type': 'storage_dialog', - }; - - spyOn(Alertify, 'fileStorageDlg').and.callFake(function() { - this.resizeTo = function() {/*This is intentional (SonarQube)*/}; - return this; - }); - - pgAdmin.FileManager.show_dialog(params); - - expect(Alertify.fileStorageDlg).toHaveBeenCalled(); - }); - }); -}); diff --git a/web/webpack.config.js b/web/webpack.config.js index 210627608..8873198a4 100644 --- a/web/webpack.config.js +++ b/web/webpack.config.js @@ -381,7 +381,6 @@ module.exports = [{ schema_diff: './pgadmin/tools/schema_diff/static/js/schema_diff_hook.js', erd_tool: './pgadmin/tools/erd/static/js/erd_tool_hook.js', psql_tool: './pgadmin/tools/psql/static/js/index.js', - file_utils: './pgadmin/misc/file_manager/static/js/utility.js', debugger: './pgadmin/tools/debugger/static/js/index.js', 'pgadmin.style': pgadminCssStyles, pgadmin: pgadminScssStyles, @@ -535,7 +534,6 @@ module.exports = [{ imports: [ 'pure|pgadmin.about', 'pure|pgadmin.preferences', - 'pure|pgadmin.file_manager', 'pure|pgadmin.settings', 'pure|pgadmin.tools.backup', 'pure|pgadmin.tools.restore', @@ -546,7 +544,7 @@ module.exports = [{ 'pure|pgadmin.tools.debugger', 'pure|pgadmin.node.pga_job', 'pure|pgadmin.tools.schema_diff', - 'pure|pgadmin.tools.storage_manager', + 'pure|pgadmin.tools.file_manager', 'pure|pgadmin.tools.search_objects', 'pure|pgadmin.tools.erd_module', 'pure|pgadmin.tools.psql_module', diff --git a/web/webpack.shim.js b/web/webpack.shim.js index 4d5d13d9b..14cc6ef91 100644 --- a/web/webpack.shim.js +++ b/web/webpack.shim.js @@ -144,7 +144,6 @@ var webpackShimConfig = { 'snap.svg': path.join(__dirname, './node_modules/snapsvg-cjs/dist/snap.svg-cjs'), 'color-picker': path.join(__dirname, './node_modules/@simonwep/pickr/dist/pickr.es5.min'), 'mousetrap': path.join(__dirname, './node_modules/mousetrap'), - 'tablesorter-metric': path.join(__dirname, './node_modules/tablesorter/dist/js/parsers/parser-metric.min'), 'pathfinding': path.join(__dirname, 'node_modules/pathfinding'), 'dagre': path.join(__dirname, 'node_modules/dagre'), 'graphlib': path.join(__dirname, 'node_modules/graphlib'), @@ -205,8 +204,6 @@ var webpackShimConfig = { 'pgadmin.browser.utils': '/browser/js/utils', 'pgadmin.browser.wizard': path.join(__dirname, './pgadmin/browser/static/js/wizard'), 'pgadmin.dashboard': path.join(__dirname, './pgadmin/dashboard/static/js/Dashboard'), - 'pgadmin.file_manager': path.join(__dirname, './pgadmin/misc/file_manager/static/js/file_manager'), - 'pgadmin.file_utility': path.join(__dirname, './pgadmin/misc/file_manager/static/js/utility'), 'pgadmin.help': path.join(__dirname, './pgadmin/help/static/js/help'), 'pgadmin.misc.explain': path.join(__dirname, './pgadmin/misc/static/explain/js/explain'), 'pgadmin.misc.cloud': path.join(__dirname, './pgadmin/misc/cloud/static/js/cloud'), @@ -278,6 +275,7 @@ var webpackShimConfig = { 'pgadmin.tools.debugger': path.join(__dirname, './pgadmin/tools/debugger/static/js/'), 'pgadmin.tools.debugger.ui': path.join(__dirname, './pgadmin/tools/debugger/static/js/debugger_ui'), 'pgadmin.tools.debugger.utils': path.join(__dirname, './pgadmin/tools/debugger/static/js/debugger_utils'), + 'pgadmin.tools.file_manager': path.join(__dirname, './pgadmin/misc/file_manager/static/js'), 'pgadmin.tools.grant_wizard': path.join(__dirname, './pgadmin/tools/grant_wizard/static/js/grant_wizard'), 'pgadmin.tools.import_export': path.join(__dirname, './pgadmin/tools/import_export/static/js/import_export'), 'pgadmin.tools.import_export_servers': path.join(__dirname, './pgadmin/tools/import_export_servers/static/js/'), @@ -286,7 +284,6 @@ var webpackShimConfig = { 'pgadmin.tools.schema_diff': path.join(__dirname, './pgadmin/tools/schema_diff/static/js/schema_diff'), 'pgadmin.tools.schema_diff_ui': path.join(__dirname, './pgadmin/tools/schema_diff/static/js/schema_diff_ui'), 'pgadmin.tools.search_objects': path.join(__dirname, './pgadmin/tools/search_objects/static/js/search_objects'), - 'pgadmin.tools.storage_manager': path.join(__dirname, './pgadmin/tools/storage_manager/static/js/storage_manager'), 'pgadmin.tools.erd_module': path.join(__dirname, './pgadmin/tools/erd/static/js/erd_module'), 'pgadmin.tools.erd': path.join(__dirname, './pgadmin/tools/erd/static/js'), 'pgadmin.tools.psql_module': path.join(__dirname, './pgadmin/tools/psql/static/js/psql_module'), @@ -315,8 +312,8 @@ var webpackShimConfig = { 'pgadmin.browser.server.variable', 'pgadmin.browser.collection', 'pgadmin.browser.node.ui', 'pgadmin.browser.datamodel', 'pgadmin.browser.menu', 'pgadmin.browser.panel', 'pgadmin', 'pgadmin.browser.frame', 'slick.pgadmin.editors', 'slick.pgadmin.formatters', - 'pgadmin.backform', 'pgadmin.backgrid', 'pgadmin.browser', 'pgadmin.file_manager', - 'pgadmin.file_utility', 'pgadmin.browser.node', + 'pgadmin.backform', 'pgadmin.backgrid', 'pgadmin.browser', + 'pgadmin.browser.node', 'pgadmin.alertifyjs', 'pgadmin.settings', 'pgadmin.preferences', 'pgadmin.sqlfoldcode', ], // Checks whether JS module is npm module or not diff --git a/web/webpack.test.config.js b/web/webpack.test.config.js index 438f6ded4..9a2add644 100644 --- a/web/webpack.test.config.js +++ b/web/webpack.test.config.js @@ -212,6 +212,7 @@ module.exports = { 'pgadmin.tools.erd': path.join(__dirname, './pgadmin/tools/erd/static/js'), 'pgadmin.tools.psql': path.join(__dirname, './pgadmin/tools/psql/static/js'), 'pgadmin.tools.sqleditor': path.join(__dirname, './pgadmin/tools/sqleditor/static/js'), + 'pgadmin.tools.file_manager': path.join(__dirname, './pgadmin/misc/file_manager/static/js'), 'pgadmin.authenticate.kerberos': path.join(__dirname, './pgadmin/authenticate/static/js/kerberos'), 'bundled_codemirror': path.join(__dirname, './pgadmin/static/bundle/codemirror'), 'tools': path.join(__dirname, './pgadmin/tools/'), diff --git a/web/yarn.lock b/web/yarn.lock index 31199445f..268a303ed 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -3200,6 +3200,11 @@ async@^3.2.0: resolved "https://registry.yarnpkg.com/async/-/async-3.2.2.tgz#2eb7671034bb2194d45d30e31e24ec7e7f9670cd" integrity sha512-H0E+qZaDEfx/FY4t7iLRv1W2fFI6+pyCeTw1uN20AQPiwqwM6ojPxHxdLv4z8hi2DtnW9BOckSspLucW7pIE5g== +attr-accept@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/attr-accept/-/attr-accept-2.2.2.tgz#646613809660110749e92f2c10833b70968d929b" + integrity sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg== + autoprefixer@^10.2.4: version "10.4.0" resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.0.tgz#c3577eb32a1079a440ec253e404eaf1eb21388c8" @@ -4221,6 +4226,14 @@ convert-source-map@~1.1.0: resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.1.3.tgz#4829c877e9fe49b3161f3bf3673888e204699860" integrity sha1-SCnId+n+SbMWHzvzZziI4gRpmGA= +convert-units@^2.3.4: + version "2.3.4" + resolved "https://registry.yarnpkg.com/convert-units/-/convert-units-2.3.4.tgz#a279f4b3cb9b5d5094beba61abc742dcb46a180d" + integrity sha512-ERHfdA0UhHJp1IpwE6PnFJx8LqG7B1ZjJ20UvVCmopEnVCfER68Tbe3kvN63dLbYXDA2xFWRE6zd4Wsf0w7POg== + dependencies: + lodash.foreach "2.3.x" + lodash.keys "2.3.x" + cookie@~0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1" @@ -4787,11 +4800,6 @@ domutils@^2.5.2, domutils@^2.6.0, domutils@^2.7.0: domelementtype "^2.2.0" domhandler "^4.2.0" -dropzone@^5.9.3: - version "5.9.3" - resolved "https://registry.yarnpkg.com/dropzone/-/dropzone-5.9.3.tgz#b3070ae090fa48cbc04c17535635537ca72d70d6" - integrity sha512-Azk8kD/2/nJIuVPK+zQ9sjKMRIpRvNyqn9XwbBHNq+iNuSccbJS6hwm1Woy0pMST0erSo0u4j+KJaodndDk4vA== - duplexer2@^0.1.2, duplexer2@~0.1.0, duplexer2@~0.1.2: version "0.1.4" resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1" @@ -5343,6 +5351,13 @@ file-entry-cache@^6.0.1: dependencies: flat-cache "^3.0.4" +file-selector@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/file-selector/-/file-selector-0.6.0.tgz#fa0a8d9007b829504db4d07dd4de0310b65287dc" + integrity sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw== + dependencies: + tslib "^2.4.0" + file-type@^12.0.0: version "12.4.2" resolved "https://registry.yarnpkg.com/file-type/-/file-type-12.4.2.tgz#a344ea5664a1d01447ee7fb1b635f72feb6169d9" @@ -6493,7 +6508,7 @@ jquery-ui@>=1.8.0, jquery-ui@^1.13.0: dependencies: jquery ">=1.8.0 <4.0.0" -jquery@>=1.2.6, "jquery@>=1.7.1 <4.0.0", jquery@>=1.8.0, "jquery@>=1.8.0 <4.0.0", jquery@^3.3.1, jquery@^3.5.0, jquery@^3.5.1, jquery@^3.6.0: +"jquery@>=1.7.1 <4.0.0", jquery@>=1.8.0, "jquery@>=1.8.0 <4.0.0", jquery@^3.3.1, jquery@^3.5.0, jquery@^3.5.1, jquery@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.6.0.tgz#c72a09f15c1bdce142f49dbf1170bdf8adac2470" integrity sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw== @@ -6910,6 +6925,44 @@ locate-path@^6.0.0: dependencies: p-locate "^5.0.0" +lodash._basebind@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/lodash._basebind/-/lodash._basebind-2.3.0.tgz#2b5bc452a0e106143b21869f233bdb587417d248" + integrity sha512-SHqM7YCuJ+BeGTs7lqpWnmdHEeF4MWxS3dksJctHFNxR81FXPOzA4bS5Vs5CpcGTkBpM8FCl+YEbQEblRw8ABg== + dependencies: + lodash._basecreate "~2.3.0" + lodash._setbinddata "~2.3.0" + lodash.isobject "~2.3.0" + +lodash._basecreate@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/lodash._basecreate/-/lodash._basecreate-2.3.0.tgz#9b88a86a4dcff7b7f3c61d83a2fcfc0671ec9de0" + integrity sha512-vwZaWldZwS2y9b99D8i9+WtgiZXbHKsBsMrpxJEqTsNW20NhJo5W8PBQkeQO9CmxuqEYn8UkMnfEM2MMT4cVrw== + dependencies: + lodash._renative "~2.3.0" + lodash.isobject "~2.3.0" + lodash.noop "~2.3.0" + +lodash._basecreatecallback@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/lodash._basecreatecallback/-/lodash._basecreatecallback-2.3.0.tgz#37b2ab17591a339e988db3259fcd46019d7ac362" + integrity sha512-Ev+pDzzfVfgbiucpXijconLGRBar7/+KNCf05kSnk4CmdDVhAy1RdbU9efCJ/o9GXI08JdUGwZ+5QJ3QX3kj0g== + dependencies: + lodash._setbinddata "~2.3.0" + lodash.bind "~2.3.0" + lodash.identity "~2.3.0" + lodash.support "~2.3.0" + +lodash._basecreatewrapper@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/lodash._basecreatewrapper/-/lodash._basecreatewrapper-2.3.0.tgz#aa0c61ad96044c3933376131483a9759c3651247" + integrity sha512-YLycQ7k8AB9Wc1EOvLNxuRWcqipDkMXq2GCgnLWQR6qtgTb3gY3LELzEpnFshrEO4LOLs+R2EpcY+uCOZaLQ8Q== + dependencies: + lodash._basecreate "~2.3.0" + lodash._setbinddata "~2.3.0" + lodash._slice "~2.3.0" + lodash.isobject "~2.3.0" + lodash._baseisequal@^3.0.0: version "3.0.7" resolved "https://registry.yarnpkg.com/lodash._baseisequal/-/lodash._baseisequal-3.0.7.tgz#d8025f76339d29342767dcc887ce5cb95a5b51f1" @@ -6924,11 +6977,59 @@ lodash._bindcallback@^3.0.0: resolved "https://registry.yarnpkg.com/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz#e531c27644cf8b57a99e17ed95b35c748789392e" integrity sha1-5THCdkTPi1epnhftlbNcdIeJOS4= +lodash._createwrapper@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/lodash._createwrapper/-/lodash._createwrapper-2.3.0.tgz#d1aae1102dadf440e8e06fc133a6edd7fe146075" + integrity sha512-XjaI/rzg9W+WO4WJDQ+PRlHD5sAMJ1RhJLuT65cBxLCb1kIYs4U20jqvTDGAWyVT3c34GYiLd9AreHYuB/8yJA== + dependencies: + lodash._basebind "~2.3.0" + lodash._basecreatewrapper "~2.3.0" + lodash.isfunction "~2.3.0" + lodash._getnative@^3.0.0: version "3.9.1" resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5" integrity sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U= +lodash._objecttypes@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/lodash._objecttypes/-/lodash._objecttypes-2.3.0.tgz#6a3ea3987dd6eeb8021b2d5c9c303549cc2bae1e" + integrity sha512-jbA6QyHt9cw3BzvbWzIcnU3Z12jSneT6xBgz3Y782CJsN1tV5aTBKrFo2B4AkeHBNaxSrbPYZZpi1Lwj3xjdtg== + +lodash._renative@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/lodash._renative/-/lodash._renative-2.3.0.tgz#77d8edd4ced26dd5971f9e15a5f772e4e317fbd3" + integrity sha512-v44MRirqYqZGK/h5UKoVqXWF2L+LUiLTU+Ogu5rHRVWJUA1uWIlHaMpG8f/OA8j++BzPMQij9+erXHtgFcbuwg== + +lodash._setbinddata@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/lodash._setbinddata/-/lodash._setbinddata-2.3.0.tgz#e5610490acd13277d59858d95b5f2727f1508f04" + integrity sha512-xMFfbF7dL+sFtrdE49uHFmfpBAEwlFtfgMp86nQRlAF6aizYL+3MTbnYMKJSkP1W501PhsgiBED5kBbZd8kR2g== + dependencies: + lodash._renative "~2.3.0" + lodash.noop "~2.3.0" + +lodash._shimkeys@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/lodash._shimkeys/-/lodash._shimkeys-2.3.0.tgz#611f93149e3e6c721096b48769ef29537ada8ba9" + integrity sha512-9Iuyi7TiWMGa/9+2rqEE+Zwye4b/U2w7Saw6UX1h6Xs88mEER+uz9FZcEBPKMVKsad9Pw5GNAcIBRnW2jNpneQ== + dependencies: + lodash._objecttypes "~2.3.0" + +lodash._slice@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/lodash._slice/-/lodash._slice-2.3.0.tgz#147198132859972e4680ca29a5992c855669aa5c" + integrity sha512-7C61GhzRUv36gTafr+RIb+AomCAYsSATEoK4OP0VkNBcwvsM022Z22AVgqjjzikeNO1U29LzsJZDvLbiNPUYvA== + +lodash.bind@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/lodash.bind/-/lodash.bind-2.3.0.tgz#c2a8e18b68e5ecc152e2b168266116fea5b016cc" + integrity sha512-goakyOo+FMN8lttMPnZ0UNlr5RlzX4IrUXyTJPT2A0tGCMXySupond9wzvDqTvVmYTcQjIKGrj8naJDS2xWAlQ== + dependencies: + lodash._createwrapper "~2.3.0" + lodash._renative "~2.3.0" + lodash._slice "~2.3.0" + lodash.debounce@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" @@ -6944,6 +7045,28 @@ lodash.flattendeep@^4.4.0: resolved "https://registry.yarnpkg.com/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2" integrity sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI= +lodash.foreach@2.3.x: + version "2.3.0" + resolved "https://registry.yarnpkg.com/lodash.foreach/-/lodash.foreach-2.3.0.tgz#083404c91e846ee77245fdf9d76519c68b2af168" + integrity sha512-yLnyptVRJd0//AbGp480grgQG9iaDIV5uOgSbpurRy1dYybPbjNTLQ3FyLEQ84buVLPG7jyaiyvpzgfOutRB3Q== + dependencies: + lodash._basecreatecallback "~2.3.0" + lodash.forown "~2.3.0" + +lodash.forown@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/lodash.forown/-/lodash.forown-2.3.0.tgz#24fb4aaf800d45fc2dc60bfec3ce04c836a3ad7f" + integrity sha512-dUnCsuQTtq3Y7bxPNoEEqjJjPL2ftLtcz2PTeRKvhbpdM514AvnqCjewHGsm/W+dwspIwa14KoWEZeizJ7smxA== + dependencies: + lodash._basecreatecallback "~2.3.0" + lodash._objecttypes "~2.3.0" + lodash.keys "~2.3.0" + +lodash.identity@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/lodash.identity/-/lodash.identity-2.3.0.tgz#6b01a210c9485355c2a913b48b6711219a173ded" + integrity sha512-NYJ2r2cwy3tkx/saqbIZEX6oQUzjWTnGRu7d/zmBjMCZos3eHBxCpbvWFWSetv8jFVrptsp6EbWjzNgBKhUoOA== + lodash.isarguments@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" @@ -6967,11 +7090,32 @@ lodash.isequal@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA= +lodash.isfunction@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/lodash.isfunction/-/lodash.isfunction-2.3.0.tgz#6b2973e47a647cf12e70d676aea13643706e5267" + integrity sha512-X5lteBYlCrVO7Qc00fxP8W90fzRp6Ax9XcHANmU3OsZHdSyIVZ9ZlX5QTTpRq8aGY+9I5Rmd0UTzTIIyWPugEQ== + +lodash.isobject@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/lodash.isobject/-/lodash.isobject-2.3.0.tgz#2e16d3fc583da9831968953f2d8e6d73434f6799" + integrity sha512-jo1pfV61C4TE8BfEzqaHj6EIKiSkFANJrB6yscwuCJMSRw5tbqjk4Gv7nJzk4Z6nFKobZjGZ8Qd41vmnwgeQqQ== + dependencies: + lodash._objecttypes "~2.3.0" + lodash.istypedarray@^3.0.0: version "3.0.6" resolved "https://registry.yarnpkg.com/lodash.istypedarray/-/lodash.istypedarray-3.0.6.tgz#c9a477498607501d8e8494d283b87c39281cef62" integrity sha1-yaR3SYYHUB2OhJTSg7h8OSgc72I= +lodash.keys@2.3.x, lodash.keys@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-2.3.0.tgz#b350f4f92caa9f45a4a2ecf018454cf2f28ae253" + integrity sha512-c0UW0ffqMxSCtoVbmVt2lERJLkEqgoOn2ejPsWXzr0ZrqRbl3uruGgwHzhtqXxi6K/ei3Ey7zimOqSwXgzazPg== + dependencies: + lodash._renative "~2.3.0" + lodash._shimkeys "~2.3.0" + lodash.isobject "~2.3.0" + lodash.keys@^3.0.0: version "3.1.2" resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-3.1.2.tgz#4dbc0472b156be50a0b286855d1bd0b0c656098a" @@ -6996,6 +7140,18 @@ lodash.merge@^4.6.2: resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== +lodash.noop@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/lodash.noop/-/lodash.noop-2.3.0.tgz#3059d628d51bbf937cd2a0b6fc3a7f212a669c2c" + integrity sha512-NpSm8HRm1WkBBWHUveDukLF4Kfb5P5E3fjHc9Qre9A11nNubozLWD2wH3UBTZbu+KSuX8aSUvy9b+PUyEceJ8g== + +lodash.support@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/lodash.support/-/lodash.support-2.3.0.tgz#7eaf038af4f0d6aab776b44aa6dcfc80334c9bfd" + integrity sha512-etc7VWbB0U3Iya8ixj2xy4sDBN3jvPX7ODi8iXtn4KkkjNpdngrdc7Vlt5jub/Vgqx6/dWtp7Ml9awhCQPYKGQ== + dependencies: + lodash._renative "~2.3.0" + lodash.truncate@^4.4.2: version "4.4.2" resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193" @@ -8245,6 +8401,15 @@ prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2: object-assign "^4.1.1" react-is "^16.8.1" +prop-types@^15.8.1: + version "15.8.1" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" + integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== + dependencies: + loose-envify "^1.4.0" + object-assign "^4.1.1" + react-is "^16.13.1" + public-encrypt@^4.0.0: version "4.0.3" resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.3.tgz#4fcc9d77a07e48ba7527e7cbe0de33d0701331e0" @@ -8554,6 +8719,15 @@ react-draggable@^4.4.4: clsx "^1.1.1" prop-types "^15.6.0" +react-dropzone@^14.2.1: + version "14.2.1" + resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-14.2.1.tgz#aad17e06290723358398a7be76fb38ecf6d77c1a" + integrity sha512-jzX6wDtAjlfwZ+Fbg+G17EszWUkQVxhMTWMfAC9qSUq7II2pKglHA8aarbFKl0mLpRPDaNUcy+HD/Sf4gkf76Q== + dependencies: + attr-accept "^2.2.2" + file-selector "^0.6.0" + prop-types "^15.8.1" + react-input-autosize@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/react-input-autosize/-/react-input-autosize-3.0.0.tgz#6b5898c790d4478d69420b55441fcc31d5c50a85" @@ -9775,13 +9949,6 @@ table@^6.0.9: string-width "^4.2.3" strip-ansi "^6.0.1" -tablesorter@^2.31.2: - version "2.31.3" - resolved "https://registry.yarnpkg.com/tablesorter/-/tablesorter-2.31.3.tgz#94c33234ba0e5d9efc5ba4e48651010a396c8b64" - integrity sha512-ueEzeKiMajDcCWnUoT1dOeNEaS1OmPh9+8J0O2Sjp3TTijMygH74EA9QNJiNkLJqULyNU0RhbKY26UMUq9iurA== - dependencies: - jquery ">=1.2.6" - tapable@^2.1.1, tapable@^2.2.0: version "2.2.1" resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" @@ -9948,6 +10115,11 @@ tslib@^2.2.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01" integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== +tslib@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" + integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== + ttf2eot@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ttf2eot/-/ttf2eot-2.0.0.tgz#8e6337a585abd1608a0c84958ab483ce69f6654b"