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