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
pull/88/head
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 #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 #7487 <https://redmine.postgresql.org/issues/7487>`_ - Added support for visualise the graph using a Pie chart in the query tool.
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 #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.
@ -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 #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 #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.
.. |home| image:: images/home.png
.. |home| image:: images/sm_home.png
Use the ``Up Arrow`` icon |uparrow| to return to the previous directory.
.. |uparrow| image:: images/uparrow.png
.. |uparrow| image:: images/sm_go_back.png
Use the ``Refresh`` icon |refresh| to display the most-recent files available.
.. |refresh| image:: images/refresh.png
.. |refresh| image:: images/sm_refresh.png
Select the ``Download`` icon |download| to download the selected file.
.. |download| image:: images/download.png
Select the ``Delete`` icon |delete| to delete the selected file or folder.
.. |delete| image:: images/delete.png
Select the ``Edit`` icon |edit| to rename a file or folder.
.. |edit| image:: images/edit.png
Use the ``Upload`` icon |upload| to upload a file.
.. |upload| image:: images/upload.png
.. |download| image:: images/sm_download.png
Use the ``New Folder`` icon |folder| to add a new folder.
.. |folder| image:: images/folder.png
Use the ``Grid View`` icon |gridview| to display all the files and folders in a grid view.
.. |gridview| image:: images/gridview.png
Use the ``Table View`` icon |tableview| to display all the files and folders in a list view.
.. |tableview| image:: images/tableview.png
Click on the check box next to *Show hidden files and folders* at the bottom of the window to view hidden files and folders.
.. |folder| image:: images/sm_new_folder.png
Use the *Format* drop down list to select the format of the files to be displayed; choose from *sql*, *csv*, or *All Files*.
Other Options
*********************
.. image:: images/sm_options.png
:alt: Other options
:align: center
.. table::
:class: longtable
:widths: 1 5
+----------------------+---------------------------------------------------------------------------------------------------+
| Menu | Behavior |
+======================+===================================================================================================+
| *Rename* | Click the *Rename* option to rename a file/folder. |
+----------------------+---------------------------------------------------------------------------------------------------+
| *Delete* | Click the *Delete* option to rename a file/folder. |
+----------------------+---------------------------------------------------------------------------------------------------+
| *Upload* | Click the *Upload* option to upload multiple files to the current folder. |
+----------------------+---------------------------------------------------------------------------------------------------+
| *List View* | Click the *List View* option to to display all the files and folders in a list view. |
+----------------------+---------------------------------------------------------------------------------------------------+
| *Grid View* | Click the *Grid View* option to to display all the files and folders in a grid view. |
+----------------------+---------------------------------------------------------------------------------------------------+
| *Show Hidden Files* | Click the *Show Hidden Files* option to view hidden files and folders. |
+----------------------+---------------------------------------------------------------------------------------------------+
You can also download backup files through *Storage Manager* at the successful completion of the backups taken through :ref:`Backup Dialog <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.

View File

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

View File

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

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) {
var w = docker || pgBrowser.docker,
p = w.findPanels('utility_props');

View File

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

View File

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

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.utils.constants import MIMETYPE_APP_JS
from .utils import get_dialog_type, get_file_type_setting
MODULE_NAME = 'settings'
@ -223,38 +224,20 @@ def get_browser_tree_state():
mimetype="application/json")
def _get_dialog_type(file_type):
"""
This function return dialog type
:param file_type:
:return: dialog type.
"""
if 'pgerd' in file_type:
return 'erd_file_type'
elif 'backup' in file_type:
return 'backup_file_type'
elif 'csv' in file_type and 'txt' in file_type:
return 'import_export_file_type'
elif 'csv' in file_type and 'txt' not in file_type:
return 'storage_manager_file_type'
else:
return 'sqleditor_file_format'
@blueprint.route("/save_file_format_setting/",
endpoint="save_file_format_setting",
methods=['POST'])
@login_required
def save_file_format_setting():
"""
This function save the selected file format.
This function save the selected file format.save_file_format_setting
:return: save file format response
"""
data = request.form if request.form else json.loads(
request.data.decode('utf-8'))
file_type = _get_dialog_type(data['allowed_file_types'])
file_type = get_dialog_type(data['allowed_file_types'])
store_setting(file_type, data['selectedFormat'])
store_setting(file_type, data['last_selected_format'])
return make_json_response(success=True,
info=data,
result=request.form)
@ -276,11 +259,5 @@ def get_file_format_setting():
except (ValueError, TypeError, KeyError):
data[k] = v
file_type = _get_dialog_type(list(data.values()))
data = Setting.query.filter_by(
user_id=current_user.id, setting=file_type).first()
if data is None:
return make_json_response(success=True, info='*')
else:
return make_json_response(success=True, info=data.value)
return make_json_response(success=True,
info=get_file_type_setting(list(data.values())))

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

View File

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

View File

@ -2835,106 +2835,6 @@ define([
].join('\n')),
});
/*
* Input File Control: This control is used with Storage Manager Dialog,
* It allows user to perform following operations:
* - Select File
* - Select Folder
* - Create File
* - Opening Storage Manager Dialog itself.
*/
Backform.FileControl = Backform.InputControl.extend({
defaults: {
type: 'text',
label: '',
min: undefined,
max: undefined,
maxlength: 255,
extraClasses: [],
dialog_title: '',
btn_primary: '',
helpMessage: null,
dialog_type: 'select_file',
},
initialize: function() {
Backform.InputControl.prototype.initialize.apply(this, arguments);
},
template: _.template([
'<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.InputControl.extend({
defaults: {

View File

@ -12,10 +12,10 @@ import Notify from '../../static/js/helpers/Notifier';
define([
'sources/gettext', 'underscore', 'jquery', 'backbone', 'backform', 'backgrid', 'alertify',
'moment', 'bignumber', 'codemirror', 'sources/utils', 'sources/keyboard_shortcuts', 'sources/select2/configure_show_on_scroll',
'sources/window', 'sources/url_for', 'bootstrap.datetimepicker', 'backgrid.filter', 'bootstrap.toggle',
'sources/window', 'bootstrap.datetimepicker', 'backgrid.filter', 'bootstrap.toggle',
], function(
gettext, _, $, Backbone, Backform, Backgrid, Alertify, moment, BigNumber, CodeMirror,
commonUtils, keyboardShortcuts, configure_show_on_scroll, pgWindow, url_for
commonUtils, keyboardShortcuts, configure_show_on_scroll, pgWindow
) {
/*
* Add mechanism in backgrid to render different types of cells in
@ -2314,125 +2314,5 @@ define([
},
});
Backgrid.Extension.SelectFileCell = Backgrid.Cell.extend({
/** @property */
className: 'file-cell',
defaults: {
supported_types: ['*'],
dialog_type: 'select_file',
dialog_title: gettext('Select file'),
type: 'text',
value: '',
placeholder: gettext('Select file...'),
disabled: false,
browse_btn_label: gettext('Select file'),
check_btn_label: gettext('Validate file'),
browse_btn_visible: true,
validate_btn_visible: true,
},
initialize: function() {
Backgrid.Cell.prototype.initialize.apply(this, arguments);
this.data = _.extend(this.defaults, this.column.toJSON());
},
template: _.template([
'<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;
});

View File

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

View File

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

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 { ExpandDialogIcon, MinimizeDialogIcon } from '../components/ExternalIcon';
const ModalContext = React.createContext({});
export const ModalContext = React.createContext({});
const MIN_HEIGHT = 190;
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 { TreeNode } from './tree_nodes';
import { isValidData } from 'sources/utils';
function manageTreeEvents(event, eventName, item) {
let d = item ? item._metadata.data : [];
let node_metadata = item ? item._metadata : {};
@ -594,6 +592,6 @@ export function findInTree(rootNode, path) {
})(rootNode);
}
let isValidTreeNodeData = isValidData;
let isValidTreeNodeData = (data) => (!_.isEmpty(data));
export { isValidTreeNodeData };

View File

@ -7,7 +7,7 @@
//
//////////////////////////////////////////////////////////////////////////
import _ from 'underscore';
import _ from 'lodash';
import $ from 'jquery';
import gettext from 'sources/gettext';
import 'wcdocker';
@ -115,14 +115,6 @@ export function findAndSetFocus(container) {
}, 200);
}
let isValidData = (data) => (!_.isUndefined(data) && !_.isNull(data));
let isFunction = (fn) => (_.isFunction(fn));
let isString = (str) => (_.isString(str));
export {
isValidData, isFunction, isString,
};
export function getEpoch(inp_date) {
let date_obj = inp_date ? inp_date : new Date();
return parseInt(date_obj.getTime()/1000);
@ -456,6 +448,10 @@ export function getBrowser() {
tem=/\brv[ :]+(\d+)/g.exec(ua) || [];
return {name:'IE', version:(tem[1]||'')};
}
if(ua.startsWith('Nwjs')) {
let nwjs = ua.split('-')[0]?.split(':');
return {name:nwjs[0], version: nwjs[1]};
}
if(M[1]==='Chrome') {
tem=ua.match(/\bOPR|Edge\/(\d+)/);
@ -480,3 +476,21 @@ export function checkTrojanSource(content, isPasteEvent) {
Notify.alert(gettext('Trojan Source Warning'), msg);
}
}
export function downloadBlob(blob, fileName) {
let urlCreator = window.URL || window.webkitURL,
downloadUrl = urlCreator.createObjectURL(blob),
link = document.createElement('a');
document.body.appendChild(link);
if (getBrowser() === 'IE' && window.navigator.msSaveBlob) {
// IE10+ : (has Blob, but not a[download] or URL)
window.navigator.msSaveBlob(blob, fileName);
} else {
link.setAttribute('href', downloadUrl);
link.setAttribute('download', fileName);
link.click();
}
document.body.removeChild(link);
}

View File

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

View File

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

View File

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

View File

@ -10,7 +10,7 @@
define([
'sources/pgadmin', 'pgadmin.tools.erd/erd_tool', 'pgadmin.browser',
'pgadmin.browser.server.privilege', 'pgadmin.node.database', 'pgadmin.node.primary_key',
'pgadmin.node.foreign_key', 'pgadmin.browser.datamodel', 'pgadmin.file_manager',
'pgadmin.node.foreign_key', 'pgadmin.browser.datamodel', 'pgadmin.tools.file_manager',
], function(
pgAdmin, ERDToolModule
) {

View File

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

View File

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

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

View File

@ -9,7 +9,7 @@
import { Box, makeStyles } from '@material-ui/core';
import _ from 'lodash';
import React, {useState, useEffect, useContext, useRef, useLayoutEffect} from 'react';
import ReactDataGrid, {Row, useRowSelection} from 'react-data-grid';
import {Row, useRowSelection} from 'react-data-grid';
import LockIcon from '@material-ui/icons/Lock';
import EditIcon from '@material-ui/icons/Edit';
import { QUERY_TOOL_EVENTS } from '../QueryToolConstants';
@ -21,51 +21,12 @@ import MapIcon from '@material-ui/icons/Map';
import { QueryToolEventsContext } from '../QueryToolComponent';
import PropTypes from 'prop-types';
import gettext from 'sources/gettext';
import PgReactDataGrid from '../../../../../../static/js/components/PgReactDataGrid';
export const ROWNUM_KEY = '$_pgadmin_rownum_key_$';
export const GRID_ROW_SELECT_KEY = '$_pgadmin_gridrowselect_key_$';
const useStyles = makeStyles((theme)=>({
root: {
height: '100%',
color: theme.palette.text.primary,
backgroundColor: theme.otherVars.qtDatagridBg,
fontSize: '12px',
border: 'none',
'--rdg-selection-color': theme.palette.primary.main,
'& .rdg-cell': {
...theme.mixins.panelBorder.right,
...theme.mixins.panelBorder.bottom,
fontWeight: 'abc',
'&[aria-colindex="1"]': {
padding: 0,
},
'&[aria-selected=true]:not([role="columnheader"]):not([aria-colindex="1"])': {
outlineWidth: '1px',
outlineOffset: '-1px',
backgroundColor: theme.palette.primary.light,
color: theme.otherVars.qtDatagridSelectFg,
}
},
'& .rdg-header-row .rdg-cell': {
padding: 0,
},
'& .rdg-header-row': {
backgroundColor: theme.palette.background.default,
fontWeight: 'normal',
},
'& .rdg-row': {
backgroundColor: theme.palette.background.default,
'&[aria-selected=true]': {
backgroundColor: theme.palette.primary.light,
color: theme.otherVars.qtDatagridSelectFg,
'& .rdg-cell:nth-child(1)': {
backgroundColor: theme.palette.primary.main,
color: theme.palette.primary.contrastText,
}
},
}
},
columnHeader: {
padding: '3px 6px',
height: '100%',
@ -408,11 +369,10 @@ export default function QueryToolDataGrid({columns, rows, totalRowCount, dataCha
return (
<DataGridExtrasContext.Provider value={{onSelectedCellChange, handleCopy}}>
<ReactDataGrid
<PgReactDataGrid
id="datagrid"
columns={readyColumns}
rows={rows}
className={classes.root}
headerRowHeight={40}
rowHeight={25}
mincolumnWidthBy={50}

View File

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

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

View File

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

View File

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

View File

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

View File

@ -144,7 +144,6 @@ var webpackShimConfig = {
'snap.svg': path.join(__dirname, './node_modules/snapsvg-cjs/dist/snap.svg-cjs'),
'color-picker': path.join(__dirname, './node_modules/@simonwep/pickr/dist/pickr.es5.min'),
'mousetrap': path.join(__dirname, './node_modules/mousetrap'),
'tablesorter-metric': path.join(__dirname, './node_modules/tablesorter/dist/js/parsers/parser-metric.min'),
'pathfinding': path.join(__dirname, 'node_modules/pathfinding'),
'dagre': path.join(__dirname, 'node_modules/dagre'),
'graphlib': path.join(__dirname, 'node_modules/graphlib'),
@ -205,8 +204,6 @@ var webpackShimConfig = {
'pgadmin.browser.utils': '/browser/js/utils',
'pgadmin.browser.wizard': path.join(__dirname, './pgadmin/browser/static/js/wizard'),
'pgadmin.dashboard': path.join(__dirname, './pgadmin/dashboard/static/js/Dashboard'),
'pgadmin.file_manager': path.join(__dirname, './pgadmin/misc/file_manager/static/js/file_manager'),
'pgadmin.file_utility': path.join(__dirname, './pgadmin/misc/file_manager/static/js/utility'),
'pgadmin.help': path.join(__dirname, './pgadmin/help/static/js/help'),
'pgadmin.misc.explain': path.join(__dirname, './pgadmin/misc/static/explain/js/explain'),
'pgadmin.misc.cloud': path.join(__dirname, './pgadmin/misc/cloud/static/js/cloud'),
@ -278,6 +275,7 @@ var webpackShimConfig = {
'pgadmin.tools.debugger': path.join(__dirname, './pgadmin/tools/debugger/static/js/'),
'pgadmin.tools.debugger.ui': path.join(__dirname, './pgadmin/tools/debugger/static/js/debugger_ui'),
'pgadmin.tools.debugger.utils': path.join(__dirname, './pgadmin/tools/debugger/static/js/debugger_utils'),
'pgadmin.tools.file_manager': path.join(__dirname, './pgadmin/misc/file_manager/static/js'),
'pgadmin.tools.grant_wizard': path.join(__dirname, './pgadmin/tools/grant_wizard/static/js/grant_wizard'),
'pgadmin.tools.import_export': path.join(__dirname, './pgadmin/tools/import_export/static/js/import_export'),
'pgadmin.tools.import_export_servers': path.join(__dirname, './pgadmin/tools/import_export_servers/static/js/'),
@ -286,7 +284,6 @@ var webpackShimConfig = {
'pgadmin.tools.schema_diff': path.join(__dirname, './pgadmin/tools/schema_diff/static/js/schema_diff'),
'pgadmin.tools.schema_diff_ui': path.join(__dirname, './pgadmin/tools/schema_diff/static/js/schema_diff_ui'),
'pgadmin.tools.search_objects': path.join(__dirname, './pgadmin/tools/search_objects/static/js/search_objects'),
'pgadmin.tools.storage_manager': path.join(__dirname, './pgadmin/tools/storage_manager/static/js/storage_manager'),
'pgadmin.tools.erd_module': path.join(__dirname, './pgadmin/tools/erd/static/js/erd_module'),
'pgadmin.tools.erd': path.join(__dirname, './pgadmin/tools/erd/static/js'),
'pgadmin.tools.psql_module': path.join(__dirname, './pgadmin/tools/psql/static/js/psql_module'),
@ -315,8 +312,8 @@ var webpackShimConfig = {
'pgadmin.browser.server.variable', 'pgadmin.browser.collection', 'pgadmin.browser.node.ui',
'pgadmin.browser.datamodel', 'pgadmin.browser.menu', 'pgadmin.browser.panel', 'pgadmin',
'pgadmin.browser.frame', 'slick.pgadmin.editors', 'slick.pgadmin.formatters',
'pgadmin.backform', 'pgadmin.backgrid', 'pgadmin.browser', 'pgadmin.file_manager',
'pgadmin.file_utility', 'pgadmin.browser.node',
'pgadmin.backform', 'pgadmin.backgrid', 'pgadmin.browser',
'pgadmin.browser.node',
'pgadmin.alertifyjs', 'pgadmin.settings', 'pgadmin.preferences', 'pgadmin.sqlfoldcode',
],
// Checks whether JS module is npm module or not

View File

@ -212,6 +212,7 @@ module.exports = {
'pgadmin.tools.erd': path.join(__dirname, './pgadmin/tools/erd/static/js'),
'pgadmin.tools.psql': path.join(__dirname, './pgadmin/tools/psql/static/js'),
'pgadmin.tools.sqleditor': path.join(__dirname, './pgadmin/tools/sqleditor/static/js'),
'pgadmin.tools.file_manager': path.join(__dirname, './pgadmin/misc/file_manager/static/js'),
'pgadmin.authenticate.kerberos': path.join(__dirname, './pgadmin/authenticate/static/js/kerberos'),
'bundled_codemirror': path.join(__dirname, './pgadmin/static/bundle/codemirror'),
'tools': path.join(__dirname, './pgadmin/tools/'),

View File

@ -3200,6 +3200,11 @@ async@^3.2.0:
resolved "https://registry.yarnpkg.com/async/-/async-3.2.2.tgz#2eb7671034bb2194d45d30e31e24ec7e7f9670cd"
integrity sha512-H0E+qZaDEfx/FY4t7iLRv1W2fFI6+pyCeTw1uN20AQPiwqwM6ojPxHxdLv4z8hi2DtnW9BOckSspLucW7pIE5g==
attr-accept@^2.2.2:
version "2.2.2"
resolved "https://registry.yarnpkg.com/attr-accept/-/attr-accept-2.2.2.tgz#646613809660110749e92f2c10833b70968d929b"
integrity sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg==
autoprefixer@^10.2.4:
version "10.4.0"
resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.0.tgz#c3577eb32a1079a440ec253e404eaf1eb21388c8"
@ -4221,6 +4226,14 @@ convert-source-map@~1.1.0:
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.1.3.tgz#4829c877e9fe49b3161f3bf3673888e204699860"
integrity sha1-SCnId+n+SbMWHzvzZziI4gRpmGA=
convert-units@^2.3.4:
version "2.3.4"
resolved "https://registry.yarnpkg.com/convert-units/-/convert-units-2.3.4.tgz#a279f4b3cb9b5d5094beba61abc742dcb46a180d"
integrity sha512-ERHfdA0UhHJp1IpwE6PnFJx8LqG7B1ZjJ20UvVCmopEnVCfER68Tbe3kvN63dLbYXDA2xFWRE6zd4Wsf0w7POg==
dependencies:
lodash.foreach "2.3.x"
lodash.keys "2.3.x"
cookie@~0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1"
@ -4787,11 +4800,6 @@ domutils@^2.5.2, domutils@^2.6.0, domutils@^2.7.0:
domelementtype "^2.2.0"
domhandler "^4.2.0"
dropzone@^5.9.3:
version "5.9.3"
resolved "https://registry.yarnpkg.com/dropzone/-/dropzone-5.9.3.tgz#b3070ae090fa48cbc04c17535635537ca72d70d6"
integrity sha512-Azk8kD/2/nJIuVPK+zQ9sjKMRIpRvNyqn9XwbBHNq+iNuSccbJS6hwm1Woy0pMST0erSo0u4j+KJaodndDk4vA==
duplexer2@^0.1.2, duplexer2@~0.1.0, duplexer2@~0.1.2:
version "0.1.4"
resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1"
@ -5343,6 +5351,13 @@ file-entry-cache@^6.0.1:
dependencies:
flat-cache "^3.0.4"
file-selector@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/file-selector/-/file-selector-0.6.0.tgz#fa0a8d9007b829504db4d07dd4de0310b65287dc"
integrity sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw==
dependencies:
tslib "^2.4.0"
file-type@^12.0.0:
version "12.4.2"
resolved "https://registry.yarnpkg.com/file-type/-/file-type-12.4.2.tgz#a344ea5664a1d01447ee7fb1b635f72feb6169d9"
@ -6493,7 +6508,7 @@ jquery-ui@>=1.8.0, jquery-ui@^1.13.0:
dependencies:
jquery ">=1.8.0 <4.0.0"
jquery@>=1.2.6, "jquery@>=1.7.1 <4.0.0", jquery@>=1.8.0, "jquery@>=1.8.0 <4.0.0", jquery@^3.3.1, jquery@^3.5.0, jquery@^3.5.1, jquery@^3.6.0:
"jquery@>=1.7.1 <4.0.0", jquery@>=1.8.0, "jquery@>=1.8.0 <4.0.0", jquery@^3.3.1, jquery@^3.5.0, jquery@^3.5.1, jquery@^3.6.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.6.0.tgz#c72a09f15c1bdce142f49dbf1170bdf8adac2470"
integrity sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw==
@ -6910,6 +6925,44 @@ locate-path@^6.0.0:
dependencies:
p-locate "^5.0.0"
lodash._basebind@~2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/lodash._basebind/-/lodash._basebind-2.3.0.tgz#2b5bc452a0e106143b21869f233bdb587417d248"
integrity sha512-SHqM7YCuJ+BeGTs7lqpWnmdHEeF4MWxS3dksJctHFNxR81FXPOzA4bS5Vs5CpcGTkBpM8FCl+YEbQEblRw8ABg==
dependencies:
lodash._basecreate "~2.3.0"
lodash._setbinddata "~2.3.0"
lodash.isobject "~2.3.0"
lodash._basecreate@~2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/lodash._basecreate/-/lodash._basecreate-2.3.0.tgz#9b88a86a4dcff7b7f3c61d83a2fcfc0671ec9de0"
integrity sha512-vwZaWldZwS2y9b99D8i9+WtgiZXbHKsBsMrpxJEqTsNW20NhJo5W8PBQkeQO9CmxuqEYn8UkMnfEM2MMT4cVrw==
dependencies:
lodash._renative "~2.3.0"
lodash.isobject "~2.3.0"
lodash.noop "~2.3.0"
lodash._basecreatecallback@~2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/lodash._basecreatecallback/-/lodash._basecreatecallback-2.3.0.tgz#37b2ab17591a339e988db3259fcd46019d7ac362"
integrity sha512-Ev+pDzzfVfgbiucpXijconLGRBar7/+KNCf05kSnk4CmdDVhAy1RdbU9efCJ/o9GXI08JdUGwZ+5QJ3QX3kj0g==
dependencies:
lodash._setbinddata "~2.3.0"
lodash.bind "~2.3.0"
lodash.identity "~2.3.0"
lodash.support "~2.3.0"
lodash._basecreatewrapper@~2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/lodash._basecreatewrapper/-/lodash._basecreatewrapper-2.3.0.tgz#aa0c61ad96044c3933376131483a9759c3651247"
integrity sha512-YLycQ7k8AB9Wc1EOvLNxuRWcqipDkMXq2GCgnLWQR6qtgTb3gY3LELzEpnFshrEO4LOLs+R2EpcY+uCOZaLQ8Q==
dependencies:
lodash._basecreate "~2.3.0"
lodash._setbinddata "~2.3.0"
lodash._slice "~2.3.0"
lodash.isobject "~2.3.0"
lodash._baseisequal@^3.0.0:
version "3.0.7"
resolved "https://registry.yarnpkg.com/lodash._baseisequal/-/lodash._baseisequal-3.0.7.tgz#d8025f76339d29342767dcc887ce5cb95a5b51f1"
@ -6924,11 +6977,59 @@ lodash._bindcallback@^3.0.0:
resolved "https://registry.yarnpkg.com/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz#e531c27644cf8b57a99e17ed95b35c748789392e"
integrity sha1-5THCdkTPi1epnhftlbNcdIeJOS4=
lodash._createwrapper@~2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/lodash._createwrapper/-/lodash._createwrapper-2.3.0.tgz#d1aae1102dadf440e8e06fc133a6edd7fe146075"
integrity sha512-XjaI/rzg9W+WO4WJDQ+PRlHD5sAMJ1RhJLuT65cBxLCb1kIYs4U20jqvTDGAWyVT3c34GYiLd9AreHYuB/8yJA==
dependencies:
lodash._basebind "~2.3.0"
lodash._basecreatewrapper "~2.3.0"
lodash.isfunction "~2.3.0"
lodash._getnative@^3.0.0:
version "3.9.1"
resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5"
integrity sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=
lodash._objecttypes@~2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/lodash._objecttypes/-/lodash._objecttypes-2.3.0.tgz#6a3ea3987dd6eeb8021b2d5c9c303549cc2bae1e"
integrity sha512-jbA6QyHt9cw3BzvbWzIcnU3Z12jSneT6xBgz3Y782CJsN1tV5aTBKrFo2B4AkeHBNaxSrbPYZZpi1Lwj3xjdtg==
lodash._renative@~2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/lodash._renative/-/lodash._renative-2.3.0.tgz#77d8edd4ced26dd5971f9e15a5f772e4e317fbd3"
integrity sha512-v44MRirqYqZGK/h5UKoVqXWF2L+LUiLTU+Ogu5rHRVWJUA1uWIlHaMpG8f/OA8j++BzPMQij9+erXHtgFcbuwg==
lodash._setbinddata@~2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/lodash._setbinddata/-/lodash._setbinddata-2.3.0.tgz#e5610490acd13277d59858d95b5f2727f1508f04"
integrity sha512-xMFfbF7dL+sFtrdE49uHFmfpBAEwlFtfgMp86nQRlAF6aizYL+3MTbnYMKJSkP1W501PhsgiBED5kBbZd8kR2g==
dependencies:
lodash._renative "~2.3.0"
lodash.noop "~2.3.0"
lodash._shimkeys@~2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/lodash._shimkeys/-/lodash._shimkeys-2.3.0.tgz#611f93149e3e6c721096b48769ef29537ada8ba9"
integrity sha512-9Iuyi7TiWMGa/9+2rqEE+Zwye4b/U2w7Saw6UX1h6Xs88mEER+uz9FZcEBPKMVKsad9Pw5GNAcIBRnW2jNpneQ==
dependencies:
lodash._objecttypes "~2.3.0"
lodash._slice@~2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/lodash._slice/-/lodash._slice-2.3.0.tgz#147198132859972e4680ca29a5992c855669aa5c"
integrity sha512-7C61GhzRUv36gTafr+RIb+AomCAYsSATEoK4OP0VkNBcwvsM022Z22AVgqjjzikeNO1U29LzsJZDvLbiNPUYvA==
lodash.bind@~2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/lodash.bind/-/lodash.bind-2.3.0.tgz#c2a8e18b68e5ecc152e2b168266116fea5b016cc"
integrity sha512-goakyOo+FMN8lttMPnZ0UNlr5RlzX4IrUXyTJPT2A0tGCMXySupond9wzvDqTvVmYTcQjIKGrj8naJDS2xWAlQ==
dependencies:
lodash._createwrapper "~2.3.0"
lodash._renative "~2.3.0"
lodash._slice "~2.3.0"
lodash.debounce@^4.0.8:
version "4.0.8"
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
@ -6944,6 +7045,28 @@ lodash.flattendeep@^4.4.0:
resolved "https://registry.yarnpkg.com/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2"
integrity sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=
lodash.foreach@2.3.x:
version "2.3.0"
resolved "https://registry.yarnpkg.com/lodash.foreach/-/lodash.foreach-2.3.0.tgz#083404c91e846ee77245fdf9d76519c68b2af168"
integrity sha512-yLnyptVRJd0//AbGp480grgQG9iaDIV5uOgSbpurRy1dYybPbjNTLQ3FyLEQ84buVLPG7jyaiyvpzgfOutRB3Q==
dependencies:
lodash._basecreatecallback "~2.3.0"
lodash.forown "~2.3.0"
lodash.forown@~2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/lodash.forown/-/lodash.forown-2.3.0.tgz#24fb4aaf800d45fc2dc60bfec3ce04c836a3ad7f"
integrity sha512-dUnCsuQTtq3Y7bxPNoEEqjJjPL2ftLtcz2PTeRKvhbpdM514AvnqCjewHGsm/W+dwspIwa14KoWEZeizJ7smxA==
dependencies:
lodash._basecreatecallback "~2.3.0"
lodash._objecttypes "~2.3.0"
lodash.keys "~2.3.0"
lodash.identity@~2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/lodash.identity/-/lodash.identity-2.3.0.tgz#6b01a210c9485355c2a913b48b6711219a173ded"
integrity sha512-NYJ2r2cwy3tkx/saqbIZEX6oQUzjWTnGRu7d/zmBjMCZos3eHBxCpbvWFWSetv8jFVrptsp6EbWjzNgBKhUoOA==
lodash.isarguments@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a"
@ -6967,11 +7090,32 @@ lodash.isequal@^4.5.0:
resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA=
lodash.isfunction@~2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/lodash.isfunction/-/lodash.isfunction-2.3.0.tgz#6b2973e47a647cf12e70d676aea13643706e5267"
integrity sha512-X5lteBYlCrVO7Qc00fxP8W90fzRp6Ax9XcHANmU3OsZHdSyIVZ9ZlX5QTTpRq8aGY+9I5Rmd0UTzTIIyWPugEQ==
lodash.isobject@~2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/lodash.isobject/-/lodash.isobject-2.3.0.tgz#2e16d3fc583da9831968953f2d8e6d73434f6799"
integrity sha512-jo1pfV61C4TE8BfEzqaHj6EIKiSkFANJrB6yscwuCJMSRw5tbqjk4Gv7nJzk4Z6nFKobZjGZ8Qd41vmnwgeQqQ==
dependencies:
lodash._objecttypes "~2.3.0"
lodash.istypedarray@^3.0.0:
version "3.0.6"
resolved "https://registry.yarnpkg.com/lodash.istypedarray/-/lodash.istypedarray-3.0.6.tgz#c9a477498607501d8e8494d283b87c39281cef62"
integrity sha1-yaR3SYYHUB2OhJTSg7h8OSgc72I=
lodash.keys@2.3.x, lodash.keys@~2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-2.3.0.tgz#b350f4f92caa9f45a4a2ecf018454cf2f28ae253"
integrity sha512-c0UW0ffqMxSCtoVbmVt2lERJLkEqgoOn2ejPsWXzr0ZrqRbl3uruGgwHzhtqXxi6K/ei3Ey7zimOqSwXgzazPg==
dependencies:
lodash._renative "~2.3.0"
lodash._shimkeys "~2.3.0"
lodash.isobject "~2.3.0"
lodash.keys@^3.0.0:
version "3.1.2"
resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-3.1.2.tgz#4dbc0472b156be50a0b286855d1bd0b0c656098a"
@ -6996,6 +7140,18 @@ lodash.merge@^4.6.2:
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
lodash.noop@~2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/lodash.noop/-/lodash.noop-2.3.0.tgz#3059d628d51bbf937cd2a0b6fc3a7f212a669c2c"
integrity sha512-NpSm8HRm1WkBBWHUveDukLF4Kfb5P5E3fjHc9Qre9A11nNubozLWD2wH3UBTZbu+KSuX8aSUvy9b+PUyEceJ8g==
lodash.support@~2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/lodash.support/-/lodash.support-2.3.0.tgz#7eaf038af4f0d6aab776b44aa6dcfc80334c9bfd"
integrity sha512-etc7VWbB0U3Iya8ixj2xy4sDBN3jvPX7ODi8iXtn4KkkjNpdngrdc7Vlt5jub/Vgqx6/dWtp7Ml9awhCQPYKGQ==
dependencies:
lodash._renative "~2.3.0"
lodash.truncate@^4.4.2:
version "4.4.2"
resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193"
@ -8245,6 +8401,15 @@ prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2:
object-assign "^4.1.1"
react-is "^16.8.1"
prop-types@^15.8.1:
version "15.8.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
dependencies:
loose-envify "^1.4.0"
object-assign "^4.1.1"
react-is "^16.13.1"
public-encrypt@^4.0.0:
version "4.0.3"
resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.3.tgz#4fcc9d77a07e48ba7527e7cbe0de33d0701331e0"
@ -8554,6 +8719,15 @@ react-draggable@^4.4.4:
clsx "^1.1.1"
prop-types "^15.6.0"
react-dropzone@^14.2.1:
version "14.2.1"
resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-14.2.1.tgz#aad17e06290723358398a7be76fb38ecf6d77c1a"
integrity sha512-jzX6wDtAjlfwZ+Fbg+G17EszWUkQVxhMTWMfAC9qSUq7II2pKglHA8aarbFKl0mLpRPDaNUcy+HD/Sf4gkf76Q==
dependencies:
attr-accept "^2.2.2"
file-selector "^0.6.0"
prop-types "^15.8.1"
react-input-autosize@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/react-input-autosize/-/react-input-autosize-3.0.0.tgz#6b5898c790d4478d69420b55441fcc31d5c50a85"
@ -9775,13 +9949,6 @@ table@^6.0.9:
string-width "^4.2.3"
strip-ansi "^6.0.1"
tablesorter@^2.31.2:
version "2.31.3"
resolved "https://registry.yarnpkg.com/tablesorter/-/tablesorter-2.31.3.tgz#94c33234ba0e5d9efc5ba4e48651010a396c8b64"
integrity sha512-ueEzeKiMajDcCWnUoT1dOeNEaS1OmPh9+8J0O2Sjp3TTijMygH74EA9QNJiNkLJqULyNU0RhbKY26UMUq9iurA==
dependencies:
jquery ">=1.2.6"
tapable@^2.1.1, tapable@^2.2.0:
version "2.2.1"
resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0"
@ -9948,6 +10115,11 @@ tslib@^2.2.0:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01"
integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==
tslib@^2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3"
integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==
ttf2eot@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ttf2eot/-/ttf2eot-2.0.0.tgz#8e6337a585abd1608a0c84958ab483ce69f6654b"