1) Port the file/storage manager to React. Fixes #7313

2) Allow users to delete files/folders from the storage manager. Fixes #4607
3) Allow users to search within the file/storage manager. Fixes #7389
4) Fixed an issue where new folders cannot be created in the save dialog. Fixes #7524
This commit is contained in:
Aditya Toshniwal 2022-07-19 15:27:47 +05:30 committed by Akshay Joshi
parent 4585597388
commit 4808df5e95
76 changed files with 2907 additions and 3927 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

View File

@ -10,12 +10,15 @@ New features
************ ************
| `Issue #4488 <https://redmine.postgresql.org/issues/4488>`_ - Added option to trigger autocomplete on key press in the query tool. | `Issue #4488 <https://redmine.postgresql.org/issues/4488>`_ - Added option to trigger autocomplete on key press in the query tool.
| `Issue #4607 <https://redmine.postgresql.org/issues/4607>`_ - Allow users to delete files/folders from the storage manager.
| `Issue #7389 <https://redmine.postgresql.org/issues/7389>`_ - Allow users to search within the file/storage manager.
| `Issue #7486 <https://redmine.postgresql.org/issues/7486>`_ - Added support for visualizing the graphs using Stacked Line, Bar, and Stacked Bar charts in the query tool. | `Issue #7486 <https://redmine.postgresql.org/issues/7486>`_ - Added support for visualizing the graphs using Stacked Line, Bar, and Stacked Bar charts in the query tool.
| `Issue #7487 <https://redmine.postgresql.org/issues/7487>`_ - Added support for visualise the graph using a Pie chart in the query tool. | `Issue #7487 <https://redmine.postgresql.org/issues/7487>`_ - Added support for visualise the graph using a Pie chart in the query tool.
Housekeeping Housekeeping
************ ************
| `Issue #7313 <https://redmine.postgresql.org/issues/7313>`_ - Port the file/storage manager to React.
| `Issue #7341 <https://redmine.postgresql.org/issues/7341>`_ - Port change password dialog to React. | `Issue #7341 <https://redmine.postgresql.org/issues/7341>`_ - Port change password dialog to React.
| `Issue #7342 <https://redmine.postgresql.org/issues/7342>`_ - Port Master Password dialog to React. | `Issue #7342 <https://redmine.postgresql.org/issues/7342>`_ - Port Master Password dialog to React.
| `Issue #7492 <https://redmine.postgresql.org/issues/7492>`_ - Removing dynamic module loading and replacing it with static loading. | `Issue #7492 <https://redmine.postgresql.org/issues/7492>`_ - Removing dynamic module loading and replacing it with static loading.
@ -34,3 +37,4 @@ Bug fixes
| `Issue #7520 <https://redmine.postgresql.org/issues/7520>`_ - Fixed the JSON editor issue of hiding the first record. | `Issue #7520 <https://redmine.postgresql.org/issues/7520>`_ - Fixed the JSON editor issue of hiding the first record.
| `Issue #7522 <https://redmine.postgresql.org/issues/7522>`_ - Added support for Azure PostgreSQL deployment in server mode. | `Issue #7522 <https://redmine.postgresql.org/issues/7522>`_ - Added support for Azure PostgreSQL deployment in server mode.
| `Issue #7523 <https://redmine.postgresql.org/issues/7523>`_ - Fixed typo error for Statistics on the table header. | `Issue #7523 <https://redmine.postgresql.org/issues/7523>`_ - Fixed typo error for Statistics on the table header.
| `Issue #7524 <https://redmine.postgresql.org/issues/7524>`_ - Fixed an issue where new folders cannot be created in the save dialog.

View File

@ -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. 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. 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. 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. Select the ``Download`` icon |download| to download the selected file.
.. |download| image:: images/download.png .. |download| image:: images/sm_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
Use the ``New Folder`` icon |folder| to add a new folder. Use the ``New Folder`` icon |folder| to add a new folder.
.. |folder| image:: images/folder.png .. |folder| image:: images/sm_new_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.
Use the *Format* drop down list to select the format of the files to be displayed; choose from *sql*, *csv*, or *All Files*. 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 <backup_dialog>`, :ref:`Backup Global Dialog <backup_globals_dialog>`, or :ref:`Backup Server Dialog <backup_server_dialog>`. You can also download backup files through *Storage Manager* at the successful completion of the backups taken through :ref:`Backup Dialog <backup_dialog>`, :ref:`Backup Global Dialog <backup_globals_dialog>`, or :ref:`Backup Server Dialog <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. 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.

View File

@ -7,6 +7,7 @@
"license": "PostgreSQL", "license": "PostgreSQL",
"chromium-args": "--disable-popup-blocking --disable-gpu", "chromium-args": "--disable-popup-blocking --disable-gpu",
"user-agent": "Nwjs:%nwver-%osinfo-%chromium_ver", "user-agent": "Nwjs:%nwver-%osinfo-%chromium_ver",
"nodejs": true,
"window": { "window": {
"width": 750, "width": 750,
"height": 600, "height": 600,

View File

@ -111,12 +111,12 @@
"closest": "^0.0.1", "closest": "^0.0.1",
"codemirror": "^5.59.2", "codemirror": "^5.59.2",
"context-menu": "^2.0.0", "context-menu": "^2.0.0",
"convert-units": "^2.3.4",
"css-loader": "^5.0.1", "css-loader": "^5.0.1",
"cssnano": "^5.0.2", "cssnano": "^5.0.2",
"dagre": "^0.8.4", "dagre": "^0.8.4",
"date-fns": "^2.24.0", "date-fns": "^2.24.0",
"diff-arrays-of-objects": "^1.1.8", "diff-arrays-of-objects": "^1.1.8",
"dropzone": "^5.9.3",
"html2canvas": "^1.0.0-rc.7", "html2canvas": "^1.0.0-rc.7",
"immutability-helper": "^3.0.0", "immutability-helper": "^3.0.0",
"imports-loader": "^2.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-data-grid": "git+https://github.com/adityatoshniwal/react-data-grid.git/#8d9bc16ddd7c419acfbbd1c1cc2b70eb9f5b453c",
"react-dom": "^17.0.1", "react-dom": "^17.0.1",
"react-draggable": "^4.4.4", "react-draggable": "^4.4.4",
"react-dropzone": "^14.2.1",
"react-leaflet": "^3.2.2", "react-leaflet": "^3.2.2",
"react-rnd": "^10.3.5", "react-rnd": "^10.3.5",
"react-router-dom": "^6.2.2", "react-router-dom": "^6.2.2",
@ -165,7 +166,6 @@
"socket.io-client": "^4.0.0", "socket.io-client": "^4.0.0",
"split.js": "^1.5.10", "split.js": "^1.5.10",
"styled-components": "^5.2.1", "styled-components": "^5.2.1",
"tablesorter": "^2.31.2",
"tempusdominus-bootstrap-4": "^5.1.2", "tempusdominus-bootstrap-4": "^5.1.2",
"tempusdominus-core": "^5.19.3", "tempusdominus-core": "^5.19.3",
"underscore": "^1.13.1", "underscore": "^1.13.1",

View File

@ -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: '<div class="obj_properties container-fluid h-100"></div>',
...params,
});
p.load(w);
},
registerUtilityPanel: function(docker) { registerUtilityPanel: function(docker) {
var w = docker || pgBrowser.docker, var w = docker || pgBrowser.docker,
p = w.findPanels('utility_props'); p = w.findPanels('utility_props');

View File

@ -9,7 +9,7 @@
define('misc.bgprocess', [ define('misc.bgprocess', [
'sources/pgadmin', 'sources/gettext', 'sources/url_for', 'underscore', 'sources/pgadmin', 'sources/gettext', 'sources/url_for', 'underscore',
'jquery', 'pgadmin.browser', 'alertify', 'jquery', 'pgadmin.browser', 'alertify', 'pgadmin.tools.file_manager',
], function( ], function(
pgAdmin, gettext, url_for, _, $, pgBrowser, Alertify pgAdmin, gettext, url_for, _, $, pgBrowser, Alertify
) { ) {
@ -625,9 +625,7 @@ define('misc.bgprocess', [
var self = this; var self = this;
if(self.current_storage_dir) { if(self.current_storage_dir) {
pgBrowser.Events.trigger( pgAdmin.Tools.FileManager.openStorageManager(self.current_storage_dir);
'pgadmin:tools:storage_manager', self.current_storage_dir
);
} }
}, },
}); });

View File

@ -28,9 +28,11 @@ from flask_babel import gettext
from flask_security import login_required from flask_security import login_required
from pgadmin.utils import PgAdminModule from pgadmin.utils import PgAdminModule
from pgadmin.utils import get_storage_directory 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.preferences import Preferences
from pgadmin.utils.constants import PREF_LABEL_OPTIONS, MIMETYPE_APP_JS 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 # Checks if platform is Windows
if _platform == "win32": if _platform == "win32":
@ -132,9 +134,9 @@ class FileManagerModule(PgAdminModule):
list: a list of url endpoints exposed to the client. list: a list of url endpoints exposed to the client.
""" """
return [ return [
'file_manager.init',
'file_manager.filemanager', 'file_manager.filemanager',
'file_manager.index', 'file_manager.index',
'file_manager.get_trans_id',
'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',
@ -196,74 +198,62 @@ def utility():
mimetype=MIMETYPE_APP_JS) 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("/<int:trans_id>/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( @blueprint.route(
"/get_trans_id", methods=["GET", "POST"], endpoint='get_trans_id' "/init", methods=["POST"], endpoint='init'
) )
@login_required @login_required
def get_trans_id(): def init_filemanager():
if len(req.data) != 0: if len(req.data) != 0:
configs = json.loads(req.data) configs = json.loads(req.data)
trans_id = Filemanager.create_new_transaction(configs) trans_id = Filemanager.create_new_transaction(configs)
global transid data = Filemanager.get_trasaction_selection(trans_id)
transid = trans_id pref = Preferences.module('file_manager')
return make_json_response( file_dialog_view = pref.preference('file_dialog_view').get()
data={'fileTransId': transid, 'status': True} 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( @blueprint.route(
"/del_trans_id/<int:trans_id>", "/delete_trans_id/<int:trans_id>",
methods=["GET", "POST"], endpoint='delete_trans_id' methods=["DELETE"], endpoint='delete_trans_id'
) )
@login_required @login_required
def delete_trans_id(trans_id): def delete_trans_id(trans_id):
@ -279,9 +269,7 @@ def delete_trans_id(trans_id):
@login_required @login_required
def save_last_directory_visited(trans_id): def save_last_directory_visited(trans_id):
blueprint.last_directory_visited.set(req.json['path']) blueprint.last_directory_visited.set(req.json['path'])
return make_json_response( return make_json_response(status=200)
data={'status': True}
)
@blueprint.route( @blueprint.route(
@ -291,9 +279,7 @@ def save_last_directory_visited(trans_id):
@login_required @login_required
def save_file_dialog_view(trans_id): def save_file_dialog_view(trans_id):
blueprint.file_dialog_view.set(req.json['view']) blueprint.file_dialog_view.set(req.json['view'])
return make_json_response( return make_json_response(status=200)
data={'status': True}
)
@blueprint.route( @blueprint.route(
@ -303,9 +289,7 @@ def save_file_dialog_view(trans_id):
@login_required @login_required
def save_show_hidden_file_option(trans_id): def save_show_hidden_file_option(trans_id):
blueprint.show_hidden_files.set(req.json['show_hidden']) blueprint.show_hidden_files.set(req.json['show_hidden'])
return make_json_response( return make_json_response(status=200)
data={'status': True}
)
class Filemanager(object): class Filemanager(object):
@ -321,14 +305,6 @@ class Filemanager(object):
def __init__(self, trans_id): def __init__(self, trans_id):
self.trans_id = 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() self.dir = get_storage_directory()
if self.dir is not None and isinstance(self.dir, list): 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) # tuples with (capabilities, files_only, folders_only, title)
capability_map = { capability_map = {
'select_file': ( 'select_file': (
['select_file', 'rename', 'upload', 'create'], ['select_file', 'rename', 'upload', 'delete'],
True, True,
False, False,
gettext("Select File") gettext("Select File")
@ -421,6 +397,8 @@ class Filemanager(object):
# get last visited directory, if not present then traverse in reverse # get last visited directory, if not present then traverse in reverse
# order to find closest parent directory # 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() last_dir = blueprint.last_directory_visited.get()
check_dir_exists = False check_dir_exists = False
if last_dir is None: if last_dir is None:
@ -436,9 +414,8 @@ class Filemanager(object):
# create configs using above configs # create configs using above configs
configs = { configs = {
# for JS json compatibility "fileroot": last_dir,
"fileroot": last_dir.replace('\\', '\\\\'), "homedir": homedir,
"homedir": homedir.replace('\\', '\\\\'),
"dialog_type": fm_type, "dialog_type": fm_type,
"title": title, "title": title,
"upload": { "upload": {
@ -499,14 +476,14 @@ class Filemanager(object):
file_manager_data = session['fileManagerData'] file_manager_data = session['fileManagerData']
# Return from the function if transaction id not found # Return from the function if transaction id not found
if str(trans_id) not in file_manager_data: 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 # Remove the information of unique transaction id
# from the session variable. # from the session variable.
file_manager_data.pop(str(trans_id), None) file_manager_data.pop(str(trans_id), None)
session['fileManagerData'] = file_manager_data session['fileManagerData'] = file_manager_data
return make_json_response(data={'status': True}) return make_json_response(status=200)
@staticmethod @staticmethod
def _get_drives_with_size(drive_name=None): def _get_drives_with_size(drive_name=None):
@ -590,7 +567,7 @@ class Filemanager(object):
:param orig_path: path after user dir :param orig_path: path after user dir
:return: :return:
""" """
files = {} files = []
for f in sorted(os.listdir(orig_path)): for f in sorted(os.listdir(orig_path)):
system_path = os.path.join(os.path.join(orig_path, f)) system_path = os.path.join(os.path.join(orig_path, f))
@ -617,14 +594,13 @@ class Filemanager(object):
if files_only == 'true': if files_only == 'true':
continue continue
file_extension = "dir" file_extension = "dir"
user_path = "{0}/".format(user_path)
# filter files based on file_type # filter files based on file_type
elif Filemanager._skip_file_extension( elif Filemanager._skip_file_extension(
file_type, supported_types, folders_only, file_extension): file_type, supported_types, folders_only, file_extension):
continue continue
# create a list of files and folders # create a list of files and folders
files[f] = { files.append({
"Filename": f, "Filename": f,
"Path": user_path, "Path": user_path,
"file_type": file_extension, "file_type": file_extension,
@ -634,7 +610,7 @@ class Filemanager(object):
"Date Modified": modified, "Date Modified": modified,
"Size": sizeof_fmt(getsize(system_path)) "Size": sizeof_fmt(getsize(system_path))
} }
} })
return files return files
@ -649,23 +625,16 @@ class Filemanager(object):
path = unquote(path) path = unquote(path)
try: Filemanager.check_access_permission(in_dir, path)
Filemanager.check_access_permission(in_dir, path) Filemanager.resume_windows_warning()
except Exception as e:
Filemanager.resume_windows_warning()
files = {
'Code': 0,
'Error': str(e)
}
return files
files = {} files = []
if (_platform == "win32" and (path == '/' or path == '\\'))\ if (_platform == "win32" and (path == '/' or path == '\\'))\
and in_dir is None: and in_dir is None:
drives = Filemanager._get_drives_with_size() drives = Filemanager._get_drives_with_size()
for drive, drive_size in drives: for drive, drive_size in drives:
path = file_name = "{0}:".format(drive) path = file_name = "{0}:".format(drive)
files[file_name] = { files.append({
"Filename": file_name, "Filename": file_name,
"Path": path, "Path": path,
"file_type": 'drive', "file_type": 'drive',
@ -675,7 +644,7 @@ class Filemanager(object):
"Date Modified": "", "Date Modified": "",
"Size": drive_size "Size": drive_size
} }
} })
Filemanager.resume_windows_warning() Filemanager.resume_windows_warning()
return files return files
@ -683,10 +652,9 @@ class Filemanager(object):
if not path_exists(orig_path): if not path_exists(orig_path):
Filemanager.resume_windows_warning() Filemanager.resume_windows_warning()
return { return make_json_response(
'Code': 0, status=404,
'Error': gettext("'{0}' file does not exist.").format(path) errormsg=gettext("'{0}' file does not exist.").format(path))
}
user_dir = path user_dir = path
folders_only = trans_data.get('folders_only', '') folders_only = trans_data.get('folders_only', '')
@ -705,11 +673,7 @@ class Filemanager(object):
if (hasattr(e, 'strerror') and if (hasattr(e, 'strerror') and
e.strerror == gettext('Permission denied')): e.strerror == gettext('Permission denied')):
err_msg = str(e.strerror) err_msg = str(e.strerror)
return unauthorized(err_msg)
files = {
'Code': 0,
'Error': err_msg
}
Filemanager.resume_windows_warning() Filemanager.resume_windows_warning()
return files return files
@ -735,9 +699,10 @@ class Filemanager(object):
# Do not allow user to access outside his storage dir # Do not allow user to access outside his storage dir
# in server mode. # in server mode.
if not orig_path.startswith(in_dir): try:
raise InternalServerError( pathlib.Path(orig_path).relative_to(in_dir)
gettext("Access denied ({0})").format(path)) except ValueError:
raise PermissionError(gettext("Access denied ({0})").format(path))
@staticmethod @staticmethod
def get_abs_path(in_dir, path): def get_abs_path(in_dir, path):
@ -789,33 +754,13 @@ class Filemanager(object):
self.dir = "" self.dir = ""
orig_path = "{0}{1}".format(self.dir, path) orig_path = "{0}{1}".format(self.dir, path)
try: Filemanager.check_access_permission(self.dir, path)
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
user_dir = path user_dir = path
thefile = { thefile = {
'Filename': split_path(orig_path)[-1], 'Filename': split_path(orig_path)[-1],
'FileType': '', 'FileType': '',
'Path': user_dir, 'Path': user_dir,
'Error': '',
'Code': 1,
'Info': '', 'Info': '',
'Properties': { 'Properties': {
date_created: '', date_created: '',
@ -827,10 +772,9 @@ class Filemanager(object):
} }
if not path_exists(orig_path): if not path_exists(orig_path):
thefile['Error'] = gettext( return make_json_response(
"'{0}' file does not exist.").format(path) status=404,
thefile['Code'] = -1 errormsg=gettext("'{0}' file does not exist.").format(path))
return thefile
if split_path(user_dir)[-1] == '/'\ if split_path(user_dir)[-1] == '/'\
or os.path.isfile(orig_path) is False: or os.path.isfile(orig_path) is False:
@ -868,19 +812,12 @@ class Filemanager(object):
Rename file or folder Rename file or folder
""" """
if not self.validate_request('rename'): 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 '' 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, old) Filemanager.check_access_permission(the_dir, new)
Filemanager.check_access_permission(the_dir, new)
except Exception as e:
res = {
'Error': str(e),
'Code': 0
}
return res
# check if it's dir # check if it's dir
if old[-1] == '/': if old[-1] == '/':
@ -906,39 +843,27 @@ class Filemanager(object):
try: try:
os.rename(oldpath_sys, newpath_sys) os.rename(oldpath_sys, newpath_sys)
except Exception as e: except Exception as e:
code = 0 return internal_server_error("{0} {1}".format(
error_msg = "{0} {1}".format( gettext('There was an error renaming the file:'), e))
gettext('There was an error renaming the file:'), e)
result = { return {
'Old Path': old, 'Old Path': old,
'Old Name': oldname, 'Old Name': oldname,
'New Path': newpath, 'New Path': newpath,
'New Name': newname, 'New Name': newname,
'Error': error_msg,
'Code': code
} }
return result
def delete(self, path=None, req=None): def delete(self, path=None, req=None):
""" """
Delete file or folder Delete file or folder
""" """
if not self.validate_request('delete'): 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 '' the_dir = self.dir if self.dir is not None else ''
orig_path = "{0}{1}".format(the_dir, path) orig_path = "{0}{1}".format(the_dir, path)
try: Filemanager.check_access_permission(the_dir, path)
Filemanager.check_access_permission(the_dir, path)
except Exception as e:
res = {
'Error': str(e),
'Code': 0
}
return res
err_msg = '' err_msg = ''
code = 1 code = 1
@ -948,23 +873,17 @@ class Filemanager(object):
else: else:
os.remove(orig_path) os.remove(orig_path)
except Exception as e: except Exception as e:
code = 0 return internal_server_error("{0} {1}".format(
err_msg = str(e.strerror) gettext('There was an error deleting the file:'), e))
result = { return make_json_response(status=200)
'Path': path,
'Error': err_msg,
'Code': code
}
return result
def add(self, req=None): def add(self, req=None):
""" """
File upload functionality File upload functionality
""" """
if not self.validate_request('upload'): 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 '' the_dir = self.dir if self.dir is not None else ''
err_msg = '' err_msg = ''
@ -986,7 +905,7 @@ class Filemanager(object):
) )
).relative_to(the_dir) ).relative_to(the_dir)
except ValueError: except ValueError:
return self.ERROR_NOT_ALLOWED return unauthorized(self.ERROR_NOT_ALLOWED['Error'])
with open(new_name, 'wb') as f: with open(new_name, 'wb') as f:
while True: while True:
@ -996,25 +915,15 @@ class Filemanager(object):
break break
f.write(data) f.write(data)
except Exception as e: except Exception as e:
code = 0 return internal_server_error("{0} {1}".format(
err_msg = str(e.strerror) if hasattr(e, 'strerror') else str(e) gettext('There was an error adding the file:'), e))
try: Filemanager.check_access_permission(the_dir, path)
Filemanager.check_access_permission(the_dir, path)
except Exception as e:
res = {
'Error': str(e),
'Code': 0
}
return res
result = { return {
'Path': path, 'Path': path,
'Name': new_name, 'Name': new_name,
'Error': err_msg,
'Code': code
} }
return result
def is_file_exist(self, path, name, req=None): def is_file_exist(self, path, name, req=None):
""" """
@ -1026,48 +935,40 @@ class Filemanager(object):
name = unquote(name) name = unquote(name)
path = unquote(path) 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) orig_path = "{0}{1}".format(the_dir, path)
if not os.path.exists(new_name): Filemanager.check_access_permission(
code = 0 the_dir, "{}{}".format(path, name))
except Exception as e:
new_name = "{0}{1}".format(orig_path, name)
if not os.path.exists(new_name):
code = 0 code = 0
if hasattr(e, 'strerror'):
err_msg = str(e.strerror)
else:
err_msg = str(e)
result = { return {
'Path': path, 'Path': path,
'Name': name, 'Name': name,
'Error': err_msg, 'Code': code,
'Code': code
} }
return result
@staticmethod @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 Utility to provide new name for folder if file
with same name already exists with same name already exists
""" """
last_char = new_name[-1] new_name = name
t_new_path = "{}/{}{}_{}".format(in_dir, path, new_name, count) count = 0
if last_char == 'r' and not path_exists(t_new_path): while True:
return t_new_path, new_name file_path = "{}{}/".format(path, new_name)
else: create_path = file_path
last_char = int(t_new_path[-1]) + 1 if in_dir != "":
new_path = "{}/{}{}_{}".format(in_dir, path, new_name, last_char) create_path = "{}/{}".format(in_dir, file_path)
if path_exists(new_path):
count += 1 if not path_exists(create_path):
return Filemanager.get_new_name(in_dir, path, new_name, count) return create_path, file_path, new_name
else: else:
return new_path, new_name count += 1
new_name = "{}_{}".format(name, count)
@staticmethod @staticmethod
def check_file_for_bom_and_binary(filename, enc="utf-8"): def check_file_for_bom_and_binary(filename, enc="utf-8"):
@ -1125,17 +1026,15 @@ class Filemanager(object):
append({os.path.basename(filename): enc}) append({os.path.basename(filename): enc})
except IOError as ex: except IOError as ex:
status = False
# we don't want to expose real path of file # we don't want to expose real path of file
# so only show error message. # so only show error message.
if ex.strerror == 'Permission denied': if ex.strerror == 'Permission denied':
err_msg = str(ex.strerror) return unauthorized(str(ex.strerror))
else: else:
err_msg = str(ex) return internal_server_error(str(ex))
except Exception as ex: except Exception as ex:
status = False return internal_server_error(str(ex))
err_msg = str(ex)
# Remove root storage path from error message # Remove root storage path from error message
# when running in Server mode # when running in Server mode
@ -1146,52 +1045,30 @@ class Filemanager(object):
return status, err_msg, is_binary, is_startswith_bom, enc 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 Functionality to create new folder
""" """
if not self.validate_request('create'): 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: try:
Filemanager.check_access_permission(the_dir, "{}{}".format( os.mkdir(create_path)
path, name))
except Exception as e: except Exception as e:
res = { return internal_server_error(str(e))
'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)
result = { result = {
'Parent': path, 'Parent': path,
'Path': new_path,
'Name': new_name, 'Name': new_name,
'Error': err_msg, 'Date Modified': time.ctime(time.time())
'Code': code
} }
return result return result
@ -1201,20 +1078,14 @@ class Filemanager(object):
Functionality to download file Functionality to download file
""" """
if not self.validate_request('download'): 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 '' the_dir = self.dir if self.dir is not None else ''
orig_path = "{0}{1}".format(the_dir, path) orig_path = "{0}{1}".format(the_dir, path)
try: Filemanager.check_access_permission(
Filemanager.check_access_permission( the_dir, "{}{}".format(path, path)
the_dir, "{}{}".format(path, path) )
)
except Exception as e:
resp = Response(str(e))
resp.headers['Content-Disposition'] = \
'attachment; filename=' + name
return resp
name = os.path.basename(path) name = os.path.basename(path)
if orig_path and len(orig_path) > 0: if orig_path and len(orig_path) > 0:
@ -1232,12 +1103,7 @@ class Filemanager(object):
def permission(self, path=None, req=None): def permission(self, path=None, req=None):
the_dir = self.dir if self.dir is not None else '' the_dir = self.dir if self.dir is not None else ''
res = {'Code': 1} res = {'Code': 1}
try: Filemanager.check_access_permission(the_dir, path)
Filemanager.check_access_permission(the_dir, path)
except Exception as e:
err_msg = str(e)
res['Code'] = 0
res['Error'] = err_msg
return res return res
@ -1272,9 +1138,12 @@ def file_manager(trans_id):
} }
mode = req.args['mode'] mode = req.args['mode']
func = getattr(my_fm, mode)
try: try:
func = getattr(my_fm, mode)
res = func(**kwargs) res = func(**kwargs)
return make_json_response(data={'result': res, 'status': True}) except PermissionError as e:
except Exception: return unauthorized(str(e))
return getattr(my_fm, mode)(**kwargs)
if type(res) == Response:
return res
return make_json_response(data={'result': res, 'status': True})

View File

@ -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 (
<FileManager
params={params}
closeModal={closeModal}
onCancel={onCancel}
onOK={onOK}
/>
);
}, {
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);
}
}
}

View File

@ -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 (
<Box className={classes.replaceOverlay}>
<Box margin={'8px'} className={classes.replaceDialog}>
<Box padding={'1rem'}>{text}{}</Box>
<Box className={modalClasses.footer}>
<DefaultButton data-test="no" startIcon={<CloseIcon />} onClick={onNo} >{gettext('No')}</DefaultButton>
<PrimaryButton data-test="yes" className={modalClasses.margin} startIcon={<CheckRoundedIcon />}
onClick={onYes} autoFocus>{gettext('Yes')}</PrimaryButton>
</Box>
</Box>
</Box>
);
}
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 (
<ErrorBoundary>
<Box display="flex" flexDirection="column" height="100%" className={modalClasses.container}>
<Box flexGrow="1" display="flex" flexDirection="column" position="relative" overflow="hidden">
<Loader message={loaderText} />
{Boolean(confirmText) && <ConfirmFile text={confirmText} onNo={()=>setConfirmFile([null, null])} onYes={onConfirmYes}/>}
<Box className={classes.toolbar}>
<PgButtonGroup size="small" style={{flexGrow: 1}}>
<PgIconButton title={gettext('Home')} onClick={async ()=>{
await openDir(fmUtilsObj.config?.options?.homedir);
}} icon={<HomeRoundedIcon />} disabled={showUploader} />
<PgIconButton title={gettext('Go Back')} onClick={async ()=>{
await openDir(fmUtilsObj.dirname(fmUtilsObj.currPath));
}} icon={<ArrowUpwardRoundedIcon />} disabled={!fmUtilsObj.dirname(fmUtilsObj.currPath) || showUploader} />
<InputText className={classes.inputFilename}
data-label="file-path"
controlProps={{maxLength: null}}
onKeyDown={async (e)=>{
if(e.code === 'Enter') {
e.preventDefault();
await openDir(path);
}
}} value={path} onChange={setPath} readonly={showUploader} />
<PgIconButton title={gettext('Refresh')} onClick={async ()=>{
await openDir();
}} icon={<SyncRoundedIcon />} disabled={showUploader} />
</PgButtonGroup>
<InputText type="search" className={classes.inputSearch} data-label="search" placeholder='Search' value={search} onChange={setSearch} />
<PgButtonGroup size="small" style={{marginLeft: '4px'}}>
{params.dialog_type == 'storage_dialog' &&
<PgIconButton title={gettext('Download')} icon={<GetAppRoundedIcon />}
onClick={onDownload} disabled={showUploader || isNoneSelected || selectedRow?.file_type == 'dir' || selectedRow?.file_type == 'drive'} />}
{fmUtilsObj.hasCapability('create') && <PgIconButton title={gettext('New Folder')} icon={<CreateNewFolderRoundedIcon />}
onClick={onAddFolder} disabled={showUploader} />}
</PgButtonGroup>
<PgButtonGroup size="small" style={{marginLeft: '4px'}}>
<PgIconButton title={gettext('Options')} icon={<MoreHorizRoundedIcon />}
name="menu-options" ref={optionsRef} onClick={toggleMenu} disabled={showUploader} />
</PgButtonGroup>
<PgMenu
anchorRef={optionsRef}
open={openMenuName=='menu-options'}
onClose={onMenuClose}
label={gettext('Options')}
>
{fmUtilsObj.hasCapability('rename') && <PgMenuItem hasCheck onClick={renameSelectedItem} disabled={isNoneSelected}>
{gettext('Rename')}
</PgMenuItem>}
{fmUtilsObj.hasCapability('delete') && <PgMenuItem hasCheck onClick={deleteSelectedItem} disabled={isNoneSelected}>
{gettext('Delete')}
</PgMenuItem>}
{fmUtilsObj.hasCapability('upload') && <>
<PgMenuDivider />
<PgMenuItem hasCheck onClick={(e)=>{
e.keepOpen = false;
setShowUploader(true);
}}>{gettext('Upload')}</PgMenuItem>
</>}
<PgMenuDivider />
<PgMenuItem hasCheck checked={viewMode == 'list'} onClick={(e)=>toggleViewMode(e, 'list')}>{gettext('List View')}</PgMenuItem>
<PgMenuItem hasCheck checked={viewMode == 'grid'} onClick={(e)=>toggleViewMode(e, 'grid')}>{gettext('Grid View')}</PgMenuItem>
<PgMenuDivider />
<PgMenuItem hasCheck checked={fmUtilsObj.showHiddenFiles} onClick={async (e)=>{
e.keepOpen = false;
fmUtilsObj.showHiddenFiles = !fmUtilsObj.showHiddenFiles;
await openDir();
}}>{gettext('Show Hidden Files')}</PgMenuItem>
</PgMenu>
</Box>
<Box flexGrow="1" display="flex" flexDirection="column" position="relative" overflow="hidden">
{showUploader &&
<Uploader fmUtilsObj={fmUtilsObj}
onClose={async (filesUploaded)=>{
setShowUploader(false);
if(filesUploaded) {
await openDir();
}
}}/>}
{viewMode == 'list' &&
<ListView key={fmUtilsObj.currPath} items={filteredItems} operation={operation} onItemEnter={onItemEnter}
onItemSelect={onItemSelect} onItemClick={onItemClick} sortColumns={sortColumns} onSortColumnsChange={setSortColumns}/>}
{viewMode == 'grid' &&
<GridView key={fmUtilsObj.currPath} items={filteredItems} operation={operation} onItemEnter={onItemEnter}
onItemSelect={onItemSelect} />}
<FormFooterMessage type={MESSAGE_TYPE.ERROR} message={errorMsg} closable onClose={()=>setErrorMsg('')} />
{params.dialog_type == 'create_file' &&
<Box className={clsx(modalClasses.footer, classes.footerSaveAs)}>
<span style={{whiteSpace: 'nowrap', marginRight: '4px'}}>Save As</span>
<InputText inputRef={saveAsRef} autoFocus style={{height: '28px'}} value={saveAs} onChange={setSaveAs} />
</Box>}
{params.dialog_type != 'select_folder' &&
<Box className={clsx(modalClasses.footer, classes.footer1)}>
<Box>{itemsText}</Box>
<Box>
<span style={{marginRight: '8px'}}>File Format</span>
<InputSelectNonSearch value={fileType} className={classes.formatSelect}
onChange={(e)=>{
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
}))} />
</Box>
</Box>}
</Box>
</Box>
<Box className={modalClasses.footer}>
<PgButtonGroup style={{flexGrow: 1}}>
</PgButtonGroup>
<DefaultButton data-test="close" startIcon={<CloseIcon />} onClick={()=>{
onCancel?.();
closeModal();
}} >{gettext('Cancel')}</DefaultButton>
{params.dialog_type != 'storage_dialog' &&
<PrimaryButton data-test="save" className={modalClasses.margin} startIcon={<CheckRoundedIcon />}
onClick={onOkClick} disabled={okBtnDisable || showUploader}>{okBtnText}</PrimaryButton>}
</Box>
</Box>
</ErrorBoundary>
);
}
FileManager.propTypes = {
params: PropTypes.object,
closeModal: PropTypes.func,
onOK: PropTypes.func,
onCancel: PropTypes.func,
};

View File

@ -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'
};

View File

@ -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 = <DescriptionIcon style={{fontSize: '2.5rem'}} />;
if(row.file_type == 'dir') {
icon = <FolderIcon style={{fontSize: '2.5rem'}} />;
} else if(row.file_type == 'drive') {
icon = <StorageRoundedIcon style={{fontSize: '2.5rem'}} />;
}
return (
<li className={classes.gridItem} aria-rowindex={idx} aria-selected={selected}>
<div className={classes.gridItemContent} aria-selected={selected} onClick={()=>onItemSelect(idx)} onDoubleClick={()=>onItemEnter(row)}>
<div>
{icon}
{Boolean(row.Protected) && <LockRoundedIcon className={classes.protected}/>}
</div>
<div ref={fileNameRef} onKeyDown={handleKeyDown} onBlur={()=>onEditComplete(row)}
className={editMode ? classes.gridItemEdit : classes.gridFilename} suppressContentEditableWarning={true}
contentEditable={editMode} data-test="filename-div">{row['Filename']}</div>
</div>
</li>
);
}
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 (
<Box flexGrow={1} overflow="hidden auto">
<ul ref={gridRef} className={classes.grid}>
{items.map((item, i)=>(
<ItemView key={i} idx={i} row={item} selected={selectedIdx==i} onItemSelect={setSelectedIdx}
onItemEnter={onItemEnter} onEditComplete={operation.idx==i ? onEditComplete : null} />)
)}
</ul>
{items.length == 0 && <Box textAlign="center" p={1}>No files/folders found</Box>}
</Box>
);
}
GridView.propTypes = {
items: PropTypes.arrayOf(PropTypes.object),
operation: PropTypes.object,
onItemSelect: PropTypes.func,
onItemEnter: PropTypes.func,
};

View File

@ -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 (
<input
className={classes.input}
value={localVal}
onChange={(e)=>{
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 <KeyboardArrowDownIcon style={{fontSize: '1.2rem'}} />;
} else if(sortDirection == 'ASC') {
return <KeyboardArrowUpIcon style={{fontSize: '1.2rem'}} />;
}
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 <div data-test='test-div' tabIndex={0} onKeyDown={handleKeyDown}></div>;
}
const onRowClick = (...args)=>{
gridUtils.onItemClick?.(props.rowIdx);
props.onRowClick?.(...args);
};
return (
<Row {...props} onKeyDown={handleKeyDown} onRowClick={onRowClick} onRowDoubleClick={(row)=>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 = <DescriptionIcon style={{fontSize: '1.2rem'}} />;
if(row.file_type == 'dir') {
icon = <FolderIcon style={{fontSize: '1.2rem'}} />;
} else if(row.file_type == 'drive') {
icon = <StorageRoundedIcon style={{fontSize: '1.2rem'}} />;
}
return <>
{icon}
{Boolean(row.Protected) && <LockRoundedIcon className={classes.protected}/>}
<span style={{marginLeft: '4px'}}>{row['Filename']}</span>
</>;
}
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 (
<GridContextUtils.Provider value={{onItemEnter, onItemSelect, onItemClick}}>
<PgReactDataGrid
gridRef={gridRef}
id="files"
className={classes.grid}
hasSelectColumn={false}
columns={columns}
rows={items}
defaultColumnOptions={{
sortable: true,
resizable: true
}}
headerRowHeight={28}
rowHeight={28}
mincolumnWidthBy={25}
enableCellSelect={false}
components={{
sortIcon: CutomSortIcon,
rowRenderer: CustomRow,
noRowsFallback: <Box textAlign="center" gridColumn="1/-1" p={1}>No files/folders found</Box>,
}}
onRowsChange={(rows)=>{
operation?.onComplete?.(rows[operation.idx], operation.idx);
}}
{...props}
/>
</GridContextUtils.Provider>
);
}
ListView.propTypes = {
items: PropTypes.arrayOf(PropTypes.object),
operation: PropTypes.object,
onItemSelect: PropTypes.func,
onItemEnter: PropTypes.func,
onItemClick: PropTypes.func,
};

View File

@ -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 (
<ListItem style={{cursor: 'auto'}}>
<Box display="flex" alignItems="flex-start">
<Box overflow="hidden" style={{overflowWrap: 'break-word'}} >{upfile.file.name}</Box>
<Box marginLeft="auto">
<PgIconButton title={gettext('Remove from list')} icon={<CloseIcon />} size="xs" noBorder onClick={removeFile} />
</Box>
</Box>
<span>{useMemo(()=>getFileSize(upfile.file.size), [])}</span>
<FormFooterMessage type={type} message={message}
closable={false} showIcon={false} textCenter={true} style={{position: 'unset', padding: '0px 0px 4px', fontSize: '0.9em'}} />
</ListItem>
);
}
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 (
<Box className={classes.root}>
<Box display="flex" justifyContent="flex-end">
<PgIconButton title={gettext('Close')} icon={<CloseIcon />} size="xs" noBorder onClick={onClose} />
</Box>
<Box display="flex" flexGrow={1} overflow="hidden">
<Box className={classes.uploadArea} {...getRootProps()}>
<input {...getInputProps()} />
<Box>{gettext('Drop files here, or click to select files.')}</Box>
<Box>{gettext('The file size limit (per file) is %s MB.', fmUtilsObj.config?.upload?.fileSizeLimit)}</Box>
</Box>
{files.length > 0 &&
<Box className={classes.uploadFilesRoot}>
<List>
{files.map((upfile)=>(
<UploadedFile key={upfile.id} upfile={upfile} removeFile={async ()=>{
dispatchFileAction({
type: 'remove',
id: upfile.id,
});
}}/>
))}
</List>
</Box>}
</Box>
</Box>
);
}
Uploader.propTypes = {
fmUtilsObj: PropTypes.object,
onClose: PropTypes.func,
};

View File

@ -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');

View File

@ -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;
});

View File

@ -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',
});
}

View File

@ -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,
};

View File

@ -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('<div class=\'storage_content\'></div>');
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 = $('<div class="storage_dialog file_selection_dlg"></div>');
this.elements.content.appendChild(this.$container.get(0));
Alertify.pgDialogBuild.apply(this);
},
hooks: {
onshow: function() {/* This is intentional (SonarQube) */},
},
};
});

View File

@ -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');

File diff suppressed because it is too large Load Diff

View File

@ -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;
}
}
}

View File

@ -23,6 +23,7 @@ from pgadmin.utils.menu import MenuItem
from pgadmin.model import db, Setting from pgadmin.model import db, Setting
from pgadmin.utils.constants import MIMETYPE_APP_JS from pgadmin.utils.constants import MIMETYPE_APP_JS
from .utils import get_dialog_type, get_file_type_setting
MODULE_NAME = 'settings' MODULE_NAME = 'settings'
@ -223,38 +224,20 @@ def get_browser_tree_state():
mimetype="application/json") 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/", @blueprint.route("/save_file_format_setting/",
endpoint="save_file_format_setting", endpoint="save_file_format_setting",
methods=['POST']) methods=['POST'])
@login_required @login_required
def save_file_format_setting(): 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 :return: save file format response
""" """
data = request.form if request.form else json.loads( data = request.form if request.form else json.loads(
request.data.decode('utf-8')) 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, return make_json_response(success=True,
info=data, info=data,
result=request.form) result=request.form)
@ -276,11 +259,5 @@ def get_file_format_setting():
except (ValueError, TypeError, KeyError): except (ValueError, TypeError, KeyError):
data[k] = v data[k] = v
file_type = _get_dialog_type(list(data.values())) return make_json_response(success=True,
info=get_file_type_setting(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)

View File

@ -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

View File

@ -25,11 +25,11 @@ import _ from 'lodash';
import gettext from 'sources/gettext'; import gettext from 'sources/gettext';
import { SCHEMA_STATE_ACTIONS, StateUtilsContext } from '.'; import { SCHEMA_STATE_ACTIONS, StateUtilsContext } from '.';
import FormView, { getFieldMetaData } from './FormView'; import FormView, { getFieldMetaData } from './FormView';
import { confirmDeleteRow } from '../helpers/legacyConnector';
import CustomPropTypes from 'sources/custom_prop_types'; import CustomPropTypes from 'sources/custom_prop_types';
import { evalFunc } from 'sources/utils'; import { evalFunc } from 'sources/utils';
import { DepListenerContext } from './DepListener'; import { DepListenerContext } from './DepListener';
import { useIsMounted } from '../custom_hooks'; import { useIsMounted } from '../custom_hooks';
import Notify from '../helpers/Notifier';
const useStyles = makeStyles((theme)=>({ const useStyles = makeStyles((theme)=>({
grid: { grid: {
@ -303,15 +303,21 @@ export default function DataGridView({
return ( return (
<PgIconButton data-test="delete-row" title={gettext('Delete row')} icon={<DeleteRoundedIcon fontSize="small" />} <PgIconButton data-test="delete-row" title={gettext('Delete row')} icon={<DeleteRoundedIcon fontSize="small" />}
onClick={()=>{ onClick={()=>{
confirmDeleteRow(()=>{ Notify.confirm(
/* Get the changes on dependent fields as well */ props.customDeleteTitle || gettext('Delete Row'),
dataDispatch({ props.customDeleteMsg || gettext('Are you sure you wish to delete this row?'),
type: SCHEMA_STATE_ACTIONS.DELETE_ROW, function() {
path: accessPath, dataDispatch({
value: row.index, type: SCHEMA_STATE_ACTIONS.DELETE_ROW,
}); path: accessPath,
value: row.index,
}, ()=>{/*This is intentional (SonarQube)*/}, props.customDeleteTitle, props.customDeleteMsg); });
return true;
},
function() {
return true;
}
);
}} className={classes.gridRowButton} disabled={!canDeleteRow} /> }} className={classes.gridRowButton} disabled={!canDeleteRow} />
); );
} }

View File

@ -125,6 +125,9 @@ basicSettings = createMuiTheme(basicSettings, {
}, },
adornedEnd: { adornedEnd: {
paddingRight: basicSettings.spacing(0.75), paddingRight: basicSettings.spacing(0.75),
},
marginDense: {
height: '28px',
} }
}, },
MuiAccordion: { MuiAccordion: {

View File

@ -2835,106 +2835,6 @@ define([
].join('\n')), ].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([
'<label class="<%=Backform.controlLabelClassName%>" for="<%=cId%>"><%=label%></label>',
'<div class="<%=Backform.controlsClassName%>">',
'<div class="input-group">',
'<input type="<%=type%>" id="<%=cId%>" class="form-control <%=extraClasses.join(\' \')%>" name="<%=name%>" min="<%=min%>" max="<%=max%>"maxlength="<%=maxlength%>" value="<%-value%>" placeholder="<%-placeholder%>" <%=disabled ? "disabled" : ""%> <%=readonly ? "readonly aria-readonly=true" : ""%> <%=required ? "required" : ""%> />',
'<div class="input-group-append">',
'<button class="btn btn-primary-icon fa fa-ellipsis-h select_item" <%=disabled ? "disabled" : ""%> <%=readonly ? "disabled" : ""%> aria-hidden="true" aria-label="' + gettext('Select file') + '" title="' + gettext('Select file') + '"></button>',
'</div>',
'</div>',
'<% if (helpMessage && helpMessage.length) { %>',
'<span class="<%=Backform.helpMessageClassName%>"><%=helpMessage%></span>',
'<% } %>',
'</div>',
].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.DatetimepickerControl =
Backform.InputControl.extend({ Backform.InputControl.extend({
defaults: { defaults: {

View File

@ -12,10 +12,10 @@ import Notify from '../../static/js/helpers/Notifier';
define([ define([
'sources/gettext', 'underscore', 'jquery', 'backbone', 'backform', 'backgrid', 'alertify', 'sources/gettext', 'underscore', 'jquery', 'backbone', 'backform', 'backgrid', 'alertify',
'moment', 'bignumber', 'codemirror', 'sources/utils', 'sources/keyboard_shortcuts', 'sources/select2/configure_show_on_scroll', '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( ], function(
gettext, _, $, Backbone, Backform, Backgrid, Alertify, moment, BigNumber, CodeMirror, 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 * 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([
'<div class="input-group">',
'<input type="<%=type%>" id="<%=cId%>" class="form-control" value="<%-value%>" placeholder="<%-placeholder%>" <%=disabled ? "disabled" : ""%> />',
'<% if (browse_btn_visible) { %>',
'<div class="input-group-append">',
'<button class="btn btn-primary-icon fa fa-ellipsis-h select_item" <%=disabled ? "disabled" : ""%> aria-hidden="true" aria-label=<%=browse_btn_label%> title=<%=browse_btn_label%>></button>',
'</div>',
'<% } %>',
'<% if (validate_btn_visible) { %>',
'<div class="input-group-append">',
'<button class="btn btn-primary-icon fa fa-clipboard-check validate_item" <%=disabled ? "disabled" : ""%> <%=(value=="" || value==null) ? "disabled" : ""%> aria-hidden="true" aria-label=<%=check_btn_label%> title=<%=check_btn_label%>></button>',
'</div>',
'<% } %>',
'</div>',
].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; return Backgrid;
}); });

View File

@ -199,7 +199,7 @@ PgIconButton.propTypes = {
export const PgButtonGroup = forwardRef(({children, ...props}, ref)=>{ export const PgButtonGroup = forwardRef(({children, ...props}, ref)=>{
/* Tooltip does not work for disabled items */ /* Tooltip does not work for disabled items */
return ( return (
<ButtonGroup disableElevation innerRef={ref} {...props}> <ButtonGroup innerRef={ref} {...props}>
{children} {children}
</ButtonGroup> </ButtonGroup>
); );

View File

@ -35,13 +35,13 @@ import * as DateFns from 'date-fns';
import CodeMirror from './CodeMirror'; import CodeMirror from './CodeMirror';
import gettext from 'sources/gettext'; import gettext from 'sources/gettext';
import { showFileDialog } from '../helpers/legacyConnector';
import _ from 'lodash'; import _ from 'lodash';
import { DefaultButton, PrimaryButton, PgIconButton } from './Buttons'; import { DefaultButton, PrimaryButton, PgIconButton } from './Buttons';
import CustomPropTypes from '../custom_prop_types'; import CustomPropTypes from '../custom_prop_types';
import KeyboardShortcuts from './KeyboardShortcuts'; import KeyboardShortcuts from './KeyboardShortcuts';
import QueryThresholds from './QueryThresholds'; import QueryThresholds from './QueryThresholds';
import SelectThemes from './SelectThemes'; import SelectThemes from './SelectThemes';
import { showFileManager } from '../helpers/showFileManager';
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
@ -326,11 +326,10 @@ FormInputDateTimePicker.propTypes = {
/* Use forwardRef to pass ref prop to OutlinedInput */ /* Use forwardRef to pass ref prop to OutlinedInput */
export const InputText = forwardRef(({ 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 maxlength = typeof(controlProps?.maxLength) != 'undefined' ? controlProps.maxLength : 255;
const classes = useStyles();
const patterns = { const patterns = {
'numeric': '^-?[0-9]\\d*\\.?\\d*$', 'numeric': '^-?[0-9]\\d*\\.?\\d*$',
'int': '^-?[0-9]\\d*$', 'int': '^-?[0-9]\\d*$',
@ -356,12 +355,17 @@ export const InputText = forwardRef(({
finalValue = controlProps.formatter.fromRaw(finalValue); 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 ( return (
<OutlinedInput <OutlinedInput
ref={ref} ref={ref}
color="primary" color="primary"
fullWidth fullWidth
className={classes.formInput} margin={size == 'small' ? 'dense' : 'none'}
inputProps={{ inputProps={{
id: cid, id: cid,
maxLength: controlProps?.multiline ? null : maxlength, maxLength: controlProps?.multiline ? null : maxlength,
@ -378,7 +382,7 @@ export const InputText = forwardRef(({
...(controlProps?.onKeyDown && { onKeyDown: controlProps.onKeyDown }) ...(controlProps?.onKeyDown && { onKeyDown: controlProps.onKeyDown })
} }
{...controlProps} {...controlProps}
{...props} {...filteredProps}
{...(['numeric', 'int'].indexOf(type) > -1 ? { type: 'tel' } : { type: type })} {...(['numeric', 'int'].indexOf(type) > -1 ? { type: 'tel' } : { type: type })}
/> />
); );
@ -394,6 +398,7 @@ InputText.propTypes = {
onChange: PropTypes.func, onChange: PropTypes.func,
controlProps: PropTypes.object, controlProps: PropTypes.object,
type: PropTypes.string, type: PropTypes.string,
size: PropTypes.string,
}; };
export function FormInputText({ hasError, required, label, className, helpMessage, testcid, ...props }) { export function FormInputText({ hasError, required, label, className, helpMessage, testcid, ...props }) {
@ -412,7 +417,6 @@ FormInputText.propTypes = {
testcid: PropTypes.string, testcid: PropTypes.string,
}; };
/* Using the existing file dialog functions using showFileDialog */
export function InputFileSelect({ controlProps, onChange, disabled, readonly, isvalidate = false, hideBrowseButton=false,validate, ...props }) { export function InputFileSelect({ controlProps, onChange, disabled, readonly, isvalidate = false, hideBrowseButton=false,validate, ...props }) {
const inpRef = useRef(); const inpRef = useRef();
let textControlProps = {}; let textControlProps = {};
@ -420,15 +424,24 @@ export function InputFileSelect({ controlProps, onChange, disabled, readonly, is
const {placeholder} = controlProps; const {placeholder} = controlProps;
textControlProps = {placeholder}; textControlProps = {placeholder};
} }
const onFileSelect = (value) => { const showFileDialog = ()=>{
onChange && onChange(decodeURI(value)); let params = {
inpRef.current.focus(); 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 ( return (
<InputText ref={inpRef} disabled={disabled} readonly={readonly} onChange={onChange} controlProps={textControlProps} {...props} endAdornment={ <InputText ref={inpRef} disabled={disabled} readonly={readonly} onChange={onChange} controlProps={textControlProps} {...props} endAdornment={
<> <>
{!hideBrowseButton && {!hideBrowseButton &&
<IconButton onClick={() => showFileDialog(controlProps, onFileSelect)} <IconButton onClick={showFileDialog}
disabled={disabled || readonly} aria-label={gettext('Select a file')}><FolderOpenRoundedIcon /></IconButton> disabled={disabled || readonly} aria-label={gettext('Select a file')}><FolderOpenRoundedIcon /></IconButton>
} }
{isvalidate && {isvalidate &&
@ -1184,6 +1197,9 @@ const useStylesFormFooter = makeStyles((theme) => ({
message: { message: {
marginLeft: theme.spacing(0.5), marginLeft: theme.spacing(0.5),
}, },
messageCenter: {
margin: 'auto',
},
closeButton: { closeButton: {
marginLeft: 'auto', 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(); const classes = useStylesFormFooter();
return ( return (
<Box className={clsx(classes.container, classes[`container${type}`])}> <Box className={clsx(classes.container, classes[`container${type}`])}>
<FormIcon type={type} className={classes[`icon${type}`]} /> {showIcon && <FormIcon type={type} className={classes[`icon${type}`]} />}
<Box className={classes.message}>{HTMLReactParse(message || '')}</Box> <Box className={textCenter ? classes.messageCenter : classes.message}>{HTMLReactParse(message || '')}</Box>
{closable && <IconButton className={clsx(classes.closeButton, classes[`icon${type}`])} onClick={onClose}> {closable && <IconButton className={clsx(classes.closeButton, classes[`icon${type}`])} onClick={onClose}>
<FormIcon close={true} /> <FormIcon close={true} />
</IconButton>} </IconButton>}
@ -1290,6 +1306,8 @@ NotifierMessage.propTypes = {
type: PropTypes.oneOf(Object.values(MESSAGE_TYPE)).isRequired, type: PropTypes.oneOf(Object.values(MESSAGE_TYPE)).isRequired,
message: PropTypes.string, message: PropTypes.string,
closable: PropTypes.bool, closable: PropTypes.bool,
showIcon: PropTypes.bool,
textCenter: PropTypes.bool,
onClose: PropTypes.func, onClose: PropTypes.func,
}; };

View File

@ -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 <ReactDataGrid
ref={gridRef}
className={clsx(finalClassName)}
{...props}
/>;
}
PgReactDataGrid.propTypes = {
gridRef: CustomPropTypes.ref,
className: CustomPropTypes.className,
hasSelectColumn: PropTypes.bool,
enableCellSelect: PropTypes.bool,
};

View File

@ -22,7 +22,7 @@ import CheckRoundedIcon from '@material-ui/icons/CheckRounded';
import { Rnd } from 'react-rnd'; import { Rnd } from 'react-rnd';
import { ExpandDialogIcon, MinimizeDialogIcon } from '../components/ExternalIcon'; import { ExpandDialogIcon, MinimizeDialogIcon } from '../components/ExternalIcon';
const ModalContext = React.createContext({}); export const ModalContext = React.createContext({});
const MIN_HEIGHT = 190; const MIN_HEIGHT = 190;
const MIN_WIDTH = 500; const MIN_WIDTH = 500;

View File

@ -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);
}

View File

@ -0,0 +1,6 @@
import pgAdmin from 'sources/pgadmin';
import 'pgadmin.tools.file_manager';
export function showFileManager(...args) {
pgAdmin.Tools.FileManager.show(...args);
}

View File

@ -14,8 +14,6 @@ import pgAdmin from 'sources/pgadmin';
import { FileType } from 'react-aspen'; import { FileType } from 'react-aspen';
import { TreeNode } from './tree_nodes'; import { TreeNode } from './tree_nodes';
import { isValidData } from 'sources/utils';
function manageTreeEvents(event, eventName, item) { function manageTreeEvents(event, eventName, item) {
let d = item ? item._metadata.data : []; let d = item ? item._metadata.data : [];
let node_metadata = item ? item._metadata : {}; let node_metadata = item ? item._metadata : {};
@ -594,6 +592,6 @@ export function findInTree(rootNode, path) {
})(rootNode); })(rootNode);
} }
let isValidTreeNodeData = isValidData; let isValidTreeNodeData = (data) => (!_.isEmpty(data));
export { isValidTreeNodeData }; export { isValidTreeNodeData };

View File

@ -7,7 +7,7 @@
// //
////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////
import _ from 'underscore'; import _ from 'lodash';
import $ from 'jquery'; import $ from 'jquery';
import gettext from 'sources/gettext'; import gettext from 'sources/gettext';
import 'wcdocker'; import 'wcdocker';
@ -115,14 +115,6 @@ export function findAndSetFocus(container) {
}, 200); }, 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) { export function getEpoch(inp_date) {
let date_obj = inp_date ? inp_date : new Date(); let date_obj = inp_date ? inp_date : new Date();
return parseInt(date_obj.getTime()/1000); return parseInt(date_obj.getTime()/1000);
@ -456,6 +448,10 @@ export function getBrowser() {
tem=/\brv[ :]+(\d+)/g.exec(ua) || []; tem=/\brv[ :]+(\d+)/g.exec(ua) || [];
return {name:'IE', version:(tem[1]||'')}; 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') { if(M[1]==='Chrome') {
tem=ua.match(/\bOPR|Edge\/(\d+)/); tem=ua.match(/\bOPR|Edge\/(\d+)/);
@ -480,3 +476,21 @@ export function checkTrojanSource(content, isPasteEvent) {
Notify.alert(gettext('Trojan Source Warning'), msg); 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);
}

View File

@ -64,9 +64,6 @@ class ToolsModule(PgAdminModule):
from .sqleditor import blueprint as module from .sqleditor import blueprint as module
app.register_blueprint(module) app.register_blueprint(module)
from .storage_manager import blueprint as module
app.register_blueprint(module)
from .user_management import blueprint as module from .user_management import blueprint as module
app.register_blueprint(module) app.register_blueprint(module)

View File

@ -17,6 +17,9 @@ import Alertify from 'pgadmin.alertifyjs';
import pgWindow from 'sources/window'; import pgWindow from 'sources/window';
import pgAdmin from 'sources/pgadmin'; import pgAdmin from 'sources/pgadmin';
import ModalProvider from '../../../../../static/js/helpers/ModalProvider';
import Theme from '../../../../../static/js/Theme';
export default class ERDTool { export default class ERDTool {
constructor(container, params) { constructor(container, params) {
this.container = document.querySelector(container); this.container = document.querySelector(container);
@ -37,13 +40,17 @@ export default class ERDTool {
}); });
ReactDOM.render( ReactDOM.render(
<BodyWidget <Theme>
params={this.params} <ModalProvider>
getDialog={getDialog} <BodyWidget
pgWindow={pgWindow} params={this.params}
pgAdmin={pgAdmin} getDialog={getDialog}
panel={panel} pgWindow={pgWindow}
alertify={Alertify} />, pgAdmin={pgAdmin}
panel={panel}
alertify={Alertify} />
</ModalProvider>
</Theme>,
this.container this.container
); );
} }

View File

@ -27,6 +27,7 @@ import 'wcdocker';
import Theme from '../../../../../../static/js/Theme'; import Theme from '../../../../../../static/js/Theme';
import TableSchema from '../../../../../../browser/server_groups/servers/databases/schemas/tables/static/js/table.ui'; import TableSchema from '../../../../../../browser/server_groups/servers/databases/schemas/tables/static/js/table.ui';
import Notify from '../../../../../../static/js/helpers/Notifier'; import Notify from '../../../../../../static/js/helpers/Notifier';
import { ModalContext } from '../../../../../../static/js/helpers/ModalProvider';
/* Custom react-diagram action for keyboard events */ /* Custom react-diagram action for keyboard events */
export class KeyboardShortcutAction extends Action { export class KeyboardShortcutAction extends Action {
@ -61,6 +62,7 @@ export class KeyboardShortcutAction extends Action {
/* The main body container for the ERD */ /* The main body container for the ERD */
export default class BodyWidget extends React.Component { export default class BodyWidget extends React.Component {
static contextType = ModalContext;
constructor() { constructor() {
super(); super();
this.state = { this.state = {
@ -214,8 +216,6 @@ export default class BodyWidget extends React.Component {
backgroundPosition: '0px 0px', 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.props.pgAdmin.Browser.onPreferencesChange('erd', () => {
this.setState({ this.setState({
preferences: this.props.pgWindow.pgAdmin.Browser.get_preferences_for_module('erd'), preferences: this.props.pgWindow.pgAdmin.Browser.get_preferences_for_module('erd'),
@ -468,11 +468,10 @@ export default class BodyWidget extends React.Component {
onLoadDiagram() { onLoadDiagram() {
var params = { var params = {
'supported_types': ['pgerd'], // file types allowed 'supported_types': ['*','pgerd'], // file types allowed
'dialog_type': 'select_file', // open select file dialog 'dialog_type': 'select_file', // open select file dialog
}; };
this.props.pgAdmin.FileManager.init(); this.props.pgAdmin.Tools.FileManager.show(params, this.openFile.bind(this), null, this.context);
this.props.pgAdmin.FileManager.show_dialog(params);
} }
openFile(fileName) { openFile(fileName) {
@ -501,13 +500,12 @@ export default class BodyWidget extends React.Component {
this.saveFile(this.state.current_file); this.saveFile(this.state.current_file);
} else { } else {
var params = { var params = {
'supported_types': ['pgerd'], 'supported_types': ['*','pgerd'],
'dialog_type': 'create_file', 'dialog_type': 'create_file',
'dialog_title': 'Save File', 'dialog_title': 'Save File',
'btn_primary': 'Save', 'btn_primary': 'Save',
}; };
this.props.pgAdmin.FileManager.init(); this.props.pgAdmin.Tools.FileManager.show(params, this.saveFile.bind(this), null, this.context);
this.props.pgAdmin.FileManager.show_dialog(params);
} }
} }

View File

@ -10,7 +10,7 @@
define([ define([
'sources/pgadmin', 'pgadmin.tools.erd/erd_tool', 'pgadmin.browser', 'sources/pgadmin', 'pgadmin.tools.erd/erd_tool', 'pgadmin.browser',
'pgadmin.browser.server.privilege', 'pgadmin.node.database', 'pgadmin.node.primary_key', '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( ], function(
pgAdmin, ERDToolModule pgAdmin, ERDToolModule
) { ) {

View File

@ -9,7 +9,6 @@
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import 'pgadmin.file_manager';
import gettext from 'sources/gettext'; import gettext from 'sources/gettext';
import Alertify from 'pgadmin.alertifyjs'; import Alertify from 'pgadmin.alertifyjs';
import Theme from 'sources/Theme'; import Theme from 'sources/Theme';

View File

@ -20,8 +20,8 @@ var wcDocker = window.wcDocker;
import pgWindow from 'sources/window'; import pgWindow from 'sources/window';
import pgAdmin from 'sources/pgadmin'; import pgAdmin from 'sources/pgadmin';
import pgBrowser from 'pgadmin.browser'; import pgBrowser from 'pgadmin.browser';
import 'pgadmin.file_manager';
import 'pgadmin.tools.user_management'; import 'pgadmin.tools.user_management';
import 'pgadmin.tools.file_manager';
import gettext from 'sources/gettext'; import gettext from 'sources/gettext';
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';

View File

@ -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); window.addEventListener('beforeunload', onBeforeUnload);
}, []); }, []);
@ -428,8 +419,9 @@ export default function QueryToolComponent({params, pgWindow, pgAdmin, selectedN
'supported_types': ['*', 'sql'], // file types allowed 'supported_types': ['*', 'sql'], // file types allowed
'dialog_type': 'select_file', // open select file dialog 'dialog_type': 'select_file', // open select file dialog
}; };
pgAdmin.FileManager.init(); pgAdmin.Tools.FileManager.show(fileParams, (fileName)=>{
pgAdmin.FileManager.show_dialog(fileParams); eventBus.current.fireEvent(QUERY_TOOL_EVENTS.LOAD_FILE, fileName);
}, null, modal);
}], }],
[QUERY_TOOL_EVENTS.TRIGGER_SAVE_FILE, (isSaveAs=false)=>{ [QUERY_TOOL_EVENTS.TRIGGER_SAVE_FILE, (isSaveAs=false)=>{
if(!isSaveAs && qtState.current_file) { if(!isSaveAs && qtState.current_file) {
@ -441,8 +433,9 @@ export default function QueryToolComponent({params, pgWindow, pgAdmin, selectedN
'dialog_title': 'Save File', 'dialog_title': 'Save File',
'btn_primary': 'Save', 'btn_primary': 'Save',
}; };
pgAdmin.FileManager.init(); pgAdmin.Tools.FileManager.show(fileParams, (fileName)=>{
pgAdmin.FileManager.show_dialog(fileParams); eventBus.current.fireEvent(QUERY_TOOL_EVENTS.SAVE_FILE, fileName);
}, null, modal);
} }
}], }],
[QUERY_TOOL_EVENTS.LOAD_FILE_DONE, fileDone], [QUERY_TOOL_EVENTS.LOAD_FILE_DONE, fileDone],

View File

@ -9,7 +9,7 @@
import { Box, makeStyles } from '@material-ui/core'; import { Box, makeStyles } from '@material-ui/core';
import _ from 'lodash'; import _ from 'lodash';
import React, {useState, useEffect, useContext, useRef, useLayoutEffect} from 'react'; 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 LockIcon from '@material-ui/icons/Lock';
import EditIcon from '@material-ui/icons/Edit'; import EditIcon from '@material-ui/icons/Edit';
import { QUERY_TOOL_EVENTS } from '../QueryToolConstants'; import { QUERY_TOOL_EVENTS } from '../QueryToolConstants';
@ -21,51 +21,12 @@ import MapIcon from '@material-ui/icons/Map';
import { QueryToolEventsContext } from '../QueryToolComponent'; import { QueryToolEventsContext } from '../QueryToolComponent';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import gettext from 'sources/gettext'; import gettext from 'sources/gettext';
import PgReactDataGrid from '../../../../../../static/js/components/PgReactDataGrid';
export const ROWNUM_KEY = '$_pgadmin_rownum_key_$'; export const ROWNUM_KEY = '$_pgadmin_rownum_key_$';
export const GRID_ROW_SELECT_KEY = '$_pgadmin_gridrowselect_key_$'; export const GRID_ROW_SELECT_KEY = '$_pgadmin_gridrowselect_key_$';
const useStyles = makeStyles((theme)=>({ 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: { columnHeader: {
padding: '3px 6px', padding: '3px 6px',
height: '100%', height: '100%',
@ -408,11 +369,10 @@ export default function QueryToolDataGrid({columns, rows, totalRowCount, dataCha
return ( return (
<DataGridExtrasContext.Provider value={{onSelectedCellChange, handleCopy}}> <DataGridExtrasContext.Provider value={{onSelectedCellChange, handleCopy}}>
<ReactDataGrid <PgReactDataGrid
id="datagrid" id="datagrid"
columns={readyColumns} columns={readyColumns}
rows={rows} rows={rows}
className={classes.root}
headerRowHeight={40} headerRowHeight={40}
rowHeight={25} rowHeight={25}
mincolumnWidthBy={50} mincolumnWidthBy={50}

View File

@ -19,6 +19,7 @@ import OrigCodeMirror from 'bundled_codemirror';
import Notifier from '../../../../../../static/js/helpers/Notifier'; import Notifier from '../../../../../../static/js/helpers/Notifier';
import { isMac } from '../../../../../../static/js/keyboard_shortcuts'; import { isMac } from '../../../../../../static/js/keyboard_shortcuts';
import { checkTrojanSource } from '../../../../../../static/js/utils'; import { checkTrojanSource } from '../../../../../../static/js/utils';
import { parseApiError } from '../../../../../../static/js/api_instance';
const useStyles = makeStyles(()=>({ const useStyles = makeStyles(()=>({
sql: { sql: {
@ -294,7 +295,7 @@ export default function Query() {
eventBus.registerListener(QUERY_TOOL_EVENTS.LOAD_FILE, (fileName)=>{ eventBus.registerListener(QUERY_TOOL_EVENTS.LOAD_FILE, (fileName)=>{
queryToolCtx.api.post(url_for('sqleditor.load_file'), { queryToolCtx.api.post(url_for('sqleditor.load_file'), {
'file_name': decodeURI(fileName), 'file_name': decodeURI(fileName),
}).then((res)=>{ }, {transformResponse: [(data) => { return data; }]}).then((res)=>{
editor.current.setValue(res.data); editor.current.setValue(res.data);
//Check the file content for Trojan Source //Check the file content for Trojan Source
checkTrojanSource(res.data); checkTrojanSource(res.data);
@ -302,7 +303,7 @@ export default function Query() {
eventBus.fireEvent(QUERY_TOOL_EVENTS.LOAD_FILE_DONE, fileName, true); eventBus.fireEvent(QUERY_TOOL_EVENTS.LOAD_FILE_DONE, fileName, true);
}).catch((err)=>{ }).catch((err)=>{
eventBus.fireEvent(QUERY_TOOL_EVENTS.LOAD_FILE_DONE, null, false); eventBus.fireEvent(QUERY_TOOL_EVENTS.LOAD_FILE_DONE, null, false);
eventBus.fireEvent(QUERY_TOOL_EVENTS.HANDLE_API_ERROR, err); Notifier.error(parseApiError(err));
}); });
}); });

View File

@ -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
)

View File

@ -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;
});

View File

@ -15,7 +15,6 @@ import {TestSchema, TestSchemaAllTypes} from './TestSchema.ui';
import pgAdmin from 'sources/pgadmin'; import pgAdmin from 'sources/pgadmin';
import {messages} from '../fake_messages'; import {messages} from '../fake_messages';
import SchemaView from '../../../pgadmin/static/js/SchemaView'; import SchemaView from '../../../pgadmin/static/js/SchemaView';
import * as legacyConnector from 'sources/helpers/legacyConnector';
import Notify from '../../../pgadmin/static/js/helpers/Notifier'; import Notify from '../../../pgadmin/static/js/helpers/Notifier';
import Theme from '../../../pgadmin/static/js/Theme'; import Theme from '../../../pgadmin/static/js/Theme';
@ -191,18 +190,17 @@ describe('SchemaView', ()=>{
simulateValidData(); simulateValidData();
/* Press OK */ /* Press OK */
let confirmSpy = spyOn(legacyConnector, 'confirmDeleteRow').and.callFake((yesFn)=>{ let confirmSpy = spyOn(Notify, 'confirm').and.callThrough();
yesFn();
});
ctrl.find('DataGridView').find('PgIconButton[data-test="delete-row"]').at(0).find('button').simulate('click'); 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'); confirmSpy.calls.argsFor(0)[2]();
expect(confirmSpy.calls.argsFor(0)[3]).toBe('Custom delete message');
expect(confirmSpy.calls.argsFor(0)[0]).toBe('Custom delete title');
expect(confirmSpy.calls.argsFor(0)[1]).toBe('Custom delete message');
/* Press Cancel */ /* Press Cancel */
spyOn(legacyConnector, 'confirmDeleteRow').and.callFake((yesFn, cancelFn)=>{ confirmSpy.calls.reset();
cancelFn();
});
ctrl.find('DataGridView').find('PgIconButton[data-test="delete-row"]').at(0).find('button').simulate('click'); ctrl.find('DataGridView').find('PgIconButton[data-test="delete-row"]').at(0).find('button').simulate('click');
confirmSpy.calls.argsFor(0)[3]();
setTimeout(()=>{ setTimeout(()=>{
ctrlUpdate(done); ctrlUpdate(done);
}, 0); }, 0);
@ -297,7 +295,7 @@ describe('SchemaView', ()=>{
}, 0); }, 0);
}); });
let onRestAction = (done, data)=> { let onResetAction = (done, data)=> {
ctrl.update(); ctrl.update();
expect(ctrl.find('DefaultButton[data-test="Reset"]').prop('disabled')).toBeTrue(); expect(ctrl.find('DefaultButton[data-test="Reset"]').prop('disabled')).toBeTrue();
expect(ctrl.find('PrimaryButton[data-test="Save"]').prop('disabled')).toBeTrue(); expect(ctrl.find('PrimaryButton[data-test="Save"]').prop('disabled')).toBeTrue();
@ -316,7 +314,7 @@ describe('SchemaView', ()=>{
/* Press OK */ /* Press OK */
confirmSpy.calls.argsFor(0)[2](); confirmSpy.calls.argsFor(0)[2]();
setTimeout(()=>{ setTimeout(()=>{
onRestAction(done, { id: undefined, field1: null, field2: null, fieldcoll: null }); onResetAction(done, { id: undefined, field1: null, field2: null, fieldcoll: null });
}, 0); }, 0);
}, 0); }, 0);
}); });
@ -390,7 +388,9 @@ describe('SchemaView', ()=>{
ctrl.find('MappedCellControl[id="field5"]').at(2).find('input').simulate('change', {target: {value: 'rval53'}}); ctrl.find('MappedCellControl[id="field5"]').at(2).find('input').simulate('change', {target: {value: 'rval53'}});
/* Remove the 1st row */ /* 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'); 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*/ /* Edit the 2nd row which is first now*/
ctrl.find('MappedCellControl[id="field5"]').at(0).find('input').simulate('change', {target: {value: 'rvalnew'}}); ctrl.find('MappedCellControl[id="field5"]').at(0).find('input').simulate('change', {target: {value: 'rvalnew'}});
@ -403,11 +403,6 @@ describe('SchemaView', ()=>{
mode: 'edit', mode: 'edit',
} }
}); });
/* Press OK */
spyOn(legacyConnector, 'confirmDeleteRow').and.callFake((yesFn)=>{
yesFn();
});
}); });
it('init', (done)=>{ it('init', (done)=>{
setTimeout(()=>{ setTimeout(()=>{
@ -463,9 +458,9 @@ describe('SchemaView', ()=>{
let confirmSpy = spyOn(Notify, 'confirm').and.callThrough(); let confirmSpy = spyOn(Notify, 'confirm').and.callThrough();
ctrl.find('DefaultButton[data-test="Reset"]').simulate('click'); ctrl.find('DefaultButton[data-test="Reset"]').simulate('click');
/* Press OK */ /* Press OK */
confirmSpy.calls.argsFor(0)[2](); confirmSpy.calls.mostRecent().args[2]();
setTimeout(()=>{ setTimeout(()=>{
onRestAction(done, {}); onResetAction(done, {});
}, 0); }, 0);
}, 0); }, 0);
}); });

View File

@ -28,10 +28,10 @@ import {FormInputText, FormInputFileSelect, FormInputSQL,
FormInputColor, FormInputColor,
FormFooterMessage, FormFooterMessage,
MESSAGE_TYPE} from '../../../pgadmin/static/js/components/FormComponents'; 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 CodeMirror from '../../../pgadmin/static/js/components/CodeMirror';
import { ToggleButton } from '@material-ui/lab'; import { ToggleButton } from '@material-ui/lab';
import { DefaultButton, PrimaryButton } from '../../../pgadmin/static/js/components/Buttons'; 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 */ /* MUI Components need to be wrapped in Theme for theme vars */
describe('FormComponents', ()=>{ describe('FormComponents', ()=>{
@ -118,7 +118,7 @@ describe('FormComponents', ()=>{
let ThemedFormInputFileSelect = withTheme(FormInputFileSelect), ctrl; let ThemedFormInputFileSelect = withTheme(FormInputFileSelect), ctrl;
beforeEach(()=>{ beforeEach(()=>{
spyOn(legacyConnector, 'showFileDialog').and.callFake((controlProps, onFileSelect)=>{ spyOn(showFileManager, 'showFileManager').and.callFake((controlProps, onFileSelect)=>{
onFileSelect('selected/file'); onFileSelect('selected/file');
}); });
ctrl = mount( ctrl = mount(

View File

@ -38,12 +38,11 @@ let pgAdmin = {
app_version_int: 1234, app_version_int: 1234,
}, },
}, },
FileManager: {
init: jasmine.createSpy(),
show_dialog: jasmine.createSpy(),
},
Tools: { Tools: {
SQLEditor: {}, SQLEditor: {},
FileManager: {
show: jasmine.createSpy(),
},
} }
}; };
@ -360,7 +359,7 @@ describe('ERD BodyWidget', ()=>{
it('onLoadDiagram', ()=>{ it('onLoadDiagram', ()=>{
bodyInstance.onLoadDiagram(); bodyInstance.onLoadDiagram();
expect(pgAdmin.FileManager.show_dialog).toHaveBeenCalled(); expect(pgAdmin.Tools.FileManager.show).toHaveBeenCalled();
}); });
it('openFile', (done)=>{ it('openFile', (done)=>{
@ -389,9 +388,10 @@ describe('ERD BodyWidget', ()=>{
done(); done();
}); });
pgAdmin.Tools.FileManager.show.calls.reset();
bodyInstance.onSaveDiagram(true); bodyInstance.onSaveDiagram(true);
expect(pgAdmin.FileManager.show_dialog).toHaveBeenCalledWith({ expect(pgAdmin.Tools.FileManager.show.calls.argsFor(0)[0]).toEqual({
'supported_types': ['pgerd'], 'supported_types': ['*','pgerd'],
'dialog_type': 'create_file', 'dialog_type': 'create_file',
'dialog_title': 'Save File', 'dialog_title': 'Save File',
'btn_primary': 'Save', 'btn_primary': 'Save',

View File

@ -28,5 +28,13 @@ define(function () {
'erd.sql': '/erd/sql/<int:trans_id>/<int:sgid>/<int:sid>/<int:did>', 'erd.sql': '/erd/sql/<int:trans_id>/<int:sgid>/<int:sid>/<int:did>',
'erd.prequisite': '/erd/prequisite/<int:trans_id>/<int:sgid>/<int:sid>/<int:did>', 'erd.prequisite': '/erd/prequisite/<int:trans_id>/<int:sgid>/<int:sid>/<int:did>',
'erd.tables': '/erd/tables/<int:trans_id>/<int:sgid>/<int:sid>/<int:did>', 'erd.tables': '/erd/tables/<int:trans_id>/<int:sgid>/<int:sid>/<int:did>',
'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/<int:trans_id>',
'file_manager.save_last_dir': '/file_manager/save_last_dir/<int:trans_id>',
'file_manager.save_file_dialog_view': '/file_manager/save_file_dialog_view/<int:trans_id>',
'file_manager.save_show_hidden_file_option': '/file_manager/save_show_hidden_file_option/<int:trans_id>',
'settings.save_file_format_setting': '/settings/save_file_format_setting/',
}; };
}); });

View File

@ -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(<Theme>
<FileManager
params={params}
closeModal={closeModal}
onOK={onOK}
onCancel={onCancel}
{...props}
/>
</Theme>);
};
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');
});
});

View File

@ -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(<Theme>
<ItemView
idx={0}
selected={false}
row={row}
{...props}
/>
</Theme>);
};
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);
});
});
});

View File

@ -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(<Theme>
<FileNameEditor
row={row}
column={column}
{...props}
/>
</Theme>);
};
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(<Theme>
<GridContextUtils.Provider value={{onItemSelect, onItemEnter}}>
<CustomRow
row={row}
selectedCellIdx={0}
rowIdx={0}
inTest={true}
/>
</GridContextUtils.Provider>
</Theme>);
};
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);
});
});
});

View File

@ -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(<Theme>
<Uploader
fmUtilsObj={fmUtilsObj}
onClose={onClose}
{...props}
/>
</Theme>);
};
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(<Theme>
<UploadedFile
deleteFile={()=>{/*dummy*/}}
onClose={onClose}
{...props}
/>
</Theme>);
};
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);
});
});
});
});

View File

@ -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();
});
});
});

View File

@ -381,7 +381,6 @@ module.exports = [{
schema_diff: './pgadmin/tools/schema_diff/static/js/schema_diff_hook.js', schema_diff: './pgadmin/tools/schema_diff/static/js/schema_diff_hook.js',
erd_tool: './pgadmin/tools/erd/static/js/erd_tool_hook.js', erd_tool: './pgadmin/tools/erd/static/js/erd_tool_hook.js',
psql_tool: './pgadmin/tools/psql/static/js/index.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', debugger: './pgadmin/tools/debugger/static/js/index.js',
'pgadmin.style': pgadminCssStyles, 'pgadmin.style': pgadminCssStyles,
pgadmin: pgadminScssStyles, pgadmin: pgadminScssStyles,
@ -535,7 +534,6 @@ module.exports = [{
imports: [ imports: [
'pure|pgadmin.about', 'pure|pgadmin.about',
'pure|pgadmin.preferences', 'pure|pgadmin.preferences',
'pure|pgadmin.file_manager',
'pure|pgadmin.settings', 'pure|pgadmin.settings',
'pure|pgadmin.tools.backup', 'pure|pgadmin.tools.backup',
'pure|pgadmin.tools.restore', 'pure|pgadmin.tools.restore',
@ -546,7 +544,7 @@ module.exports = [{
'pure|pgadmin.tools.debugger', 'pure|pgadmin.tools.debugger',
'pure|pgadmin.node.pga_job', 'pure|pgadmin.node.pga_job',
'pure|pgadmin.tools.schema_diff', 'pure|pgadmin.tools.schema_diff',
'pure|pgadmin.tools.storage_manager', 'pure|pgadmin.tools.file_manager',
'pure|pgadmin.tools.search_objects', 'pure|pgadmin.tools.search_objects',
'pure|pgadmin.tools.erd_module', 'pure|pgadmin.tools.erd_module',
'pure|pgadmin.tools.psql_module', 'pure|pgadmin.tools.psql_module',

View File

@ -144,7 +144,6 @@ var webpackShimConfig = {
'snap.svg': path.join(__dirname, './node_modules/snapsvg-cjs/dist/snap.svg-cjs'), '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'), 'color-picker': path.join(__dirname, './node_modules/@simonwep/pickr/dist/pickr.es5.min'),
'mousetrap': path.join(__dirname, './node_modules/mousetrap'), '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'), 'pathfinding': path.join(__dirname, 'node_modules/pathfinding'),
'dagre': path.join(__dirname, 'node_modules/dagre'), 'dagre': path.join(__dirname, 'node_modules/dagre'),
'graphlib': path.join(__dirname, 'node_modules/graphlib'), 'graphlib': path.join(__dirname, 'node_modules/graphlib'),
@ -205,8 +204,6 @@ var webpackShimConfig = {
'pgadmin.browser.utils': '/browser/js/utils', 'pgadmin.browser.utils': '/browser/js/utils',
'pgadmin.browser.wizard': path.join(__dirname, './pgadmin/browser/static/js/wizard'), 'pgadmin.browser.wizard': path.join(__dirname, './pgadmin/browser/static/js/wizard'),
'pgadmin.dashboard': path.join(__dirname, './pgadmin/dashboard/static/js/Dashboard'), '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.help': path.join(__dirname, './pgadmin/help/static/js/help'),
'pgadmin.misc.explain': path.join(__dirname, './pgadmin/misc/static/explain/js/explain'), 'pgadmin.misc.explain': path.join(__dirname, './pgadmin/misc/static/explain/js/explain'),
'pgadmin.misc.cloud': path.join(__dirname, './pgadmin/misc/cloud/static/js/cloud'), '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': 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.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.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.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': 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/'), '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': 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.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.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_module': path.join(__dirname, './pgadmin/tools/erd/static/js/erd_module'),
'pgadmin.tools.erd': path.join(__dirname, './pgadmin/tools/erd/static/js'), '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'), '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.server.variable', 'pgadmin.browser.collection', 'pgadmin.browser.node.ui',
'pgadmin.browser.datamodel', 'pgadmin.browser.menu', 'pgadmin.browser.panel', 'pgadmin', 'pgadmin.browser.datamodel', 'pgadmin.browser.menu', 'pgadmin.browser.panel', 'pgadmin',
'pgadmin.browser.frame', 'slick.pgadmin.editors', 'slick.pgadmin.formatters', 'pgadmin.browser.frame', 'slick.pgadmin.editors', 'slick.pgadmin.formatters',
'pgadmin.backform', 'pgadmin.backgrid', 'pgadmin.browser', 'pgadmin.file_manager', 'pgadmin.backform', 'pgadmin.backgrid', 'pgadmin.browser',
'pgadmin.file_utility', 'pgadmin.browser.node', 'pgadmin.browser.node',
'pgadmin.alertifyjs', 'pgadmin.settings', 'pgadmin.preferences', 'pgadmin.sqlfoldcode', 'pgadmin.alertifyjs', 'pgadmin.settings', 'pgadmin.preferences', 'pgadmin.sqlfoldcode',
], ],
// Checks whether JS module is npm module or not // Checks whether JS module is npm module or not

View File

@ -212,6 +212,7 @@ module.exports = {
'pgadmin.tools.erd': path.join(__dirname, './pgadmin/tools/erd/static/js'), 'pgadmin.tools.erd': path.join(__dirname, './pgadmin/tools/erd/static/js'),
'pgadmin.tools.psql': path.join(__dirname, './pgadmin/tools/psql/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.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'), 'pgadmin.authenticate.kerberos': path.join(__dirname, './pgadmin/authenticate/static/js/kerberos'),
'bundled_codemirror': path.join(__dirname, './pgadmin/static/bundle/codemirror'), 'bundled_codemirror': path.join(__dirname, './pgadmin/static/bundle/codemirror'),
'tools': path.join(__dirname, './pgadmin/tools/'), 'tools': path.join(__dirname, './pgadmin/tools/'),

View File

@ -3200,6 +3200,11 @@ async@^3.2.0:
resolved "https://registry.yarnpkg.com/async/-/async-3.2.2.tgz#2eb7671034bb2194d45d30e31e24ec7e7f9670cd" resolved "https://registry.yarnpkg.com/async/-/async-3.2.2.tgz#2eb7671034bb2194d45d30e31e24ec7e7f9670cd"
integrity sha512-H0E+qZaDEfx/FY4t7iLRv1W2fFI6+pyCeTw1uN20AQPiwqwM6ojPxHxdLv4z8hi2DtnW9BOckSspLucW7pIE5g== 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: autoprefixer@^10.2.4:
version "10.4.0" version "10.4.0"
resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.0.tgz#c3577eb32a1079a440ec253e404eaf1eb21388c8" 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" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.1.3.tgz#4829c877e9fe49b3161f3bf3673888e204699860"
integrity sha1-SCnId+n+SbMWHzvzZziI4gRpmGA= 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: cookie@~0.4.1:
version "0.4.1" version "0.4.1"
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1" 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" domelementtype "^2.2.0"
domhandler "^4.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: duplexer2@^0.1.2, duplexer2@~0.1.0, duplexer2@~0.1.2:
version "0.1.4" version "0.1.4"
resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1" resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1"
@ -5343,6 +5351,13 @@ file-entry-cache@^6.0.1:
dependencies: dependencies:
flat-cache "^3.0.4" 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: file-type@^12.0.0:
version "12.4.2" version "12.4.2"
resolved "https://registry.yarnpkg.com/file-type/-/file-type-12.4.2.tgz#a344ea5664a1d01447ee7fb1b635f72feb6169d9" 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: dependencies:
jquery ">=1.8.0 <4.0.0" 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" version "3.6.0"
resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.6.0.tgz#c72a09f15c1bdce142f49dbf1170bdf8adac2470" resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.6.0.tgz#c72a09f15c1bdce142f49dbf1170bdf8adac2470"
integrity sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw== integrity sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw==
@ -6910,6 +6925,44 @@ locate-path@^6.0.0:
dependencies: dependencies:
p-locate "^5.0.0" 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: lodash._baseisequal@^3.0.0:
version "3.0.7" version "3.0.7"
resolved "https://registry.yarnpkg.com/lodash._baseisequal/-/lodash._baseisequal-3.0.7.tgz#d8025f76339d29342767dcc887ce5cb95a5b51f1" 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" resolved "https://registry.yarnpkg.com/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz#e531c27644cf8b57a99e17ed95b35c748789392e"
integrity sha1-5THCdkTPi1epnhftlbNcdIeJOS4= 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: lodash._getnative@^3.0.0:
version "3.9.1" version "3.9.1"
resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5" resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5"
integrity sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U= 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: lodash.debounce@^4.0.8:
version "4.0.8" version "4.0.8"
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" 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" resolved "https://registry.yarnpkg.com/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2"
integrity sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI= 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: lodash.isarguments@^3.0.0:
version "3.1.0" version "3.1.0"
resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" 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" resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA= 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: lodash.istypedarray@^3.0.0:
version "3.0.6" version "3.0.6"
resolved "https://registry.yarnpkg.com/lodash.istypedarray/-/lodash.istypedarray-3.0.6.tgz#c9a477498607501d8e8494d283b87c39281cef62" resolved "https://registry.yarnpkg.com/lodash.istypedarray/-/lodash.istypedarray-3.0.6.tgz#c9a477498607501d8e8494d283b87c39281cef62"
integrity sha1-yaR3SYYHUB2OhJTSg7h8OSgc72I= 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: lodash.keys@^3.0.0:
version "3.1.2" version "3.1.2"
resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-3.1.2.tgz#4dbc0472b156be50a0b286855d1bd0b0c656098a" 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" resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== 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: lodash.truncate@^4.4.2:
version "4.4.2" version "4.4.2"
resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193" 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" object-assign "^4.1.1"
react-is "^16.8.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: public-encrypt@^4.0.0:
version "4.0.3" version "4.0.3"
resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.3.tgz#4fcc9d77a07e48ba7527e7cbe0de33d0701331e0" 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" clsx "^1.1.1"
prop-types "^15.6.0" 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: react-input-autosize@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/react-input-autosize/-/react-input-autosize-3.0.0.tgz#6b5898c790d4478d69420b55441fcc31d5c50a85" 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" string-width "^4.2.3"
strip-ansi "^6.0.1" 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: tapable@^2.1.1, tapable@^2.2.0:
version "2.2.1" version "2.2.1"
resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" 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" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01"
integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== 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: ttf2eot@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/ttf2eot/-/ttf2eot-2.0.0.tgz#8e6337a585abd1608a0c84958ab483ce69f6654b" resolved "https://registry.yarnpkg.com/ttf2eot/-/ttf2eot-2.0.0.tgz#8e6337a585abd1608a0c84958ab483ce69f6654b"