Added support to import/export server groups and servers from GUI. Fixes #4803

This commit is contained in:
Akshay Joshi 2022-01-04 12:27:17 +05:30
parent c1ad7d81f4
commit 9dd957a2aa
22 changed files with 1297 additions and 329 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

View File

@ -1,4 +1,4 @@
.. _export_import_servers:
.. _import_export_servers:
******************************
`Import/Export Servers`:index:
@ -6,14 +6,62 @@
Server definitions (and their groups) can be exported to a JSON file and
re-imported to the same or a different system to enable easy pre-configuration
of pgAdmin. The ``setup.py`` script is used for this purpose.
of pgAdmin.
.. note:: To export or import servers, you must use the Python interpreter that
is normally used to run pgAdmin to ensure that the required Python
packages are available. In most packages, this can be found in the
Python Virtual Environment that can be found in the installation
directory. When using platform-native packages, the system installation
of Python may be the one used by pgAdmin.
Using pgAdmin 4 GUI
###################
To launch the *Import/Export Servers...* tool, navigate through *Tools* on the
menu bar to click on the *Import/Export Servers* option.
.. image:: images/import_export_servers_step1.png
:alt: Import/Export Servers step one page
:align: center
* Use the *Import/Export* field to select the Server Groups/Servers
to be imported or exported.
* Use the *Filename* field to select the JSON file to import servers or create the
new file in case of Export where the servers to be exported in the JSON format.
* Use the *Replace existing servers?* field to specify whether to replace the
existing servers or not. This field is applicable only in case of Import Servers.
Click the *Next* button to continue, or the *X* button to close the wizard.
.. image:: images/import_export_servers_step2.png
:alt: Import/Export Servers step two page
:align: center
* Select the Server Groups/ Servers to be imported/exported.
Click the *Next* button to continue, or the *X* button to close the wizard.
.. image:: images/import_export_servers_step3.png
:alt: Import/Export Servers step three page
:align: center
Check the summary of the imported/exported servers on the Summary page.
Click the *Finish* button to close the wizard.
.. image:: images/import_export_servers_refresh_tree.png
:alt: Import/Export Servers Tree Refresh
:align: center
In case of importing the server above confirmation box will be popped up to
confirm whether to refresh the browser tree or later.
Using 'setup.py' command line script
####################################
.. note:: To export or import servers using ``setup.py`` script, you must use
the Python interpreter that is normally used to run pgAdmin to ensure
that the required Python packages are available. In most packages, this
can be found in the Python Virtual Environment that can be found in the
installation directory. When using platform-native packages, the system
installation of Python may be the one used by pgAdmin.
Exporting Servers
*****************

View File

@ -9,6 +9,7 @@ This release contains a number of bug fixes and new features since the release o
New features
************
| `Issue #4803 <https://redmine.postgresql.org/issues/4803>`_ - Added support to import/export server groups and servers from GUI.
Housekeeping
************
@ -21,7 +22,7 @@ Bug fixes
| `Issue #6745 <https://redmine.postgresql.org/issues/6745>`_ - Fixed an issue where Tablespace is created though an error is shown on the dialog.
| `Issue #7003 <https://redmine.postgresql.org/issues/7003>`_ - Fixed an issue where Explain Analyze shows negative exclusive time.
| `Issue #7034 <https://redmine.postgresql.org/issues/7034>`_ - Fixed an issue where Columns with default value not showing when adding a new row.
| `Issue #7075 <https://redmine.postgresql.org/issues/7075>`_ - Ensure that help should be visible properly for Procedures.
| `Issue #7075 <https://redmine.postgresql.org/issues/7075>`_ - Ensure that help should be visible properly for Procedures.
| `Issue #7077 <https://redmine.postgresql.org/issues/7077>`_ - Fixed an issue where the Owner is not displayed in the reverse engineering SQL for Procedures.
| `Issue #7078 <https://redmine.postgresql.org/issues/7078>`_ - Fixed an issue where an operation error message pop up showing the database object's name incorrectly.
| `Issue #7081 <https://redmine.postgresql.org/issues/7081>`_ - Fixed an issue in SQL generation for PostgreSQL-14 functions.

View File

@ -79,7 +79,7 @@ The context-sensitive menus associated with *Tables* and nested *Table* nodes pr
+-------------------------+------------------------------------------------------------------------------------------------------------------------------+
| Option | Action |
+=========================+==============================================================================================================================+
| *Import/Export...* | Click open the :ref:`Import/Export... <import_export_data>` dialog to import data to or export data from the selected table. |
| *Import/Export Data...* | Click open the :ref:`Import/Export... <import_export_data>` dialog to import data to or export data from the selected table. |
+-------------------------+------------------------------------------------------------------------------------------------------------------------------+
| *Reset Statistics* | Click to reset statistics for the selected table. |
+-------------------------+------------------------------------------------------------------------------------------------------------------------------+

View File

@ -143,6 +143,7 @@
"raf": "^3.4.1",
"react": "^17.0.1",
"react-aspen": "^1.1.0",
"react-checkbox-tree": "^1.7.2",
"react-dom": "^17.0.1",
"react-draggable": "^4.4.4",
"react-select": "^4.2.1",

View File

@ -25,3 +25,4 @@
@import 'node_modules/xterm/css/xterm.css';
@import 'node_modules/jsoneditor/dist/jsoneditor.min.css';
@import 'node_modules/react-checkbox-tree/lib/react-checkbox-tree.css';

View File

@ -0,0 +1,73 @@
import React from 'react';
import { makeStyles } from '@material-ui/core/styles';
import CheckboxTree from 'react-checkbox-tree';
import CheckBoxIcon from '@material-ui/icons/CheckBox';
import CheckBoxOutlineBlankIcon from '@material-ui/icons/CheckBoxOutlineBlank';
import IndeterminateCheckBoxIcon from '@material-ui/icons/IndeterminateCheckBox';
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
import ChevronRightIcon from '@material-ui/icons/ChevronRight';
import PropTypes from 'prop-types';
const useStyles = makeStyles((theme) =>
({
treeRoot: {
'& .rct-collapse, .rct-checkbox': {
padding: 0
},
'& .rct-node-leaf':{
padding: '0 0 0 10px'
},
'& .react-checkbox-tree': {
height: '97%',
fontSize: '0.815rem',
overflow: 'auto',
...theme.mixins.panelBorder
},
height: '100%'
},
unchecked: {
fill: theme.otherVars.borderColor
},
checked: {
fill: theme.palette.primary.main
}
})
);
export default function CheckBoxTree({treeData, ...props}) {
const [checked, setChecked] = React.useState([]);
const [expanded, setExpanded] = React.useState([]);
const classes = useStyles();
React.useEffect(() => {
if (props.getSelectedServers) {
props.getSelectedServers(checked);
}
}, [checked]);
return (
<div className={classes.treeRoot}>
<CheckboxTree
nodes={treeData}
checked={checked}
expanded={expanded}
onCheck={checked => setChecked(checked)}
onExpand={expanded => setExpanded(expanded)}
showNodeIcon={false}
icons={{
check: <CheckBoxIcon className={classes.checked}/>,
uncheck: <CheckBoxOutlineBlankIcon className={classes.unchecked}/>,
halfCheck: <IndeterminateCheckBoxIcon className={classes.checked}/>,
expandClose: <ChevronRightIcon />,
expandOpen: <ExpandMoreIcon />,
leaf: <ChevronRightIcon />
}}
/>
</div>
);
}
CheckBoxTree.propTypes = {
treeData: PropTypes.array,
getSelectedServers: PropTypes.func
};

View File

@ -0,0 +1,201 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2021, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################
"""A blueprint module implementing the import and export servers
functionality"""
import json
import os
import random
from flask import url_for, Response, render_template, request
from flask_babel import gettext as _
from flask_security import login_required, current_user
from pgadmin.utils import PgAdminModule
from pgadmin.utils.ajax import bad_request
from pgadmin.utils.constants import MIMETYPE_APP_JS
from web.pgadmin.utils.ajax import make_json_response, internal_server_error
from pgadmin.model import ServerGroup, Server
from pgadmin.utils import clear_database_servers, dump_database_servers,\
load_database_servers
MODULE_NAME = 'import_export_servers'
class ImportExportServersModule(PgAdminModule):
"""
class ImportExportServersModule(PgAdminModule)
A module class for import which is derived from PgAdminModule.
Methods:
-------
* get_own_javascripts(self)
- Method is used to load the required javascript files for import module
"""
LABEL = _('Import/Export Servers')
def get_own_javascripts(self):
scripts = list()
for name, script in [
['pgadmin.tools.import_export_servers', 'js/import_export_servers']
]:
scripts.append({
'name': name,
'path': url_for('import_export_servers.index') + script,
'when': None
})
return scripts
def get_exposed_url_endpoints(self):
"""
Returns:
list: URL endpoints for backup module
"""
return ['import_export_servers.get_servers',
'import_export_servers.load_servers',
'import_export_servers.save']
blueprint = ImportExportServersModule(MODULE_NAME, __name__)
@blueprint.route("/")
@login_required
def index():
return bad_request(errormsg=_("This URL cannot be called directly."))
@blueprint.route("/js/import_export_servers.js")
@login_required
def script():
"""render the import/export javascript file"""
return Response(
response=render_template(
"import_export_servers/js/import_export_servers.js", _=_),
status=200,
mimetype=MIMETYPE_APP_JS
)
@blueprint.route('/get_servers', methods=['GET'], endpoint='get_servers')
@login_required
def get_servers():
"""
This function is used to get the servers with server groups
"""
all_servers = []
groups = ServerGroup.query.filter_by(
user_id=current_user.id
).order_by("id")
# Loop through all the server groups
for idx, group in enumerate(groups):
children = []
# Loop through all the servers for specific server group
servers = Server.query.filter(
Server.user_id == current_user.id,
Server.servergroup_id == group.id)
for server in servers:
children.append({'value': server.id, 'label': server.name})
all_servers.append(
{'value': group.name, 'label': group.name, 'children': children})
return make_json_response(success=1, data=all_servers)
@blueprint.route('/load_servers', methods=['POST'], endpoint='load_servers')
@login_required
def load_servers():
"""
This function is used to load the servers from the json file.
"""
filename = None
groups = {}
all_servers = []
data = request.form if request.form else json.loads(request.data.decode())
if 'filename' in data:
filename = data['filename']
if filename is not None and os.path.exists(filename):
try:
with open(filename, 'r') as j:
data = json.loads(j.read())
if 'Servers' in data:
for server in data["Servers"]:
obj = data["Servers"][server]
server_id = server + '_' + str(random.randint(1, 9999))
if obj['Group'] in groups:
groups[obj['Group']]['children'].append(
{'value': server_id,
'label': obj['Name']})
else:
groups[obj['Group']] = \
{'value': obj['Group'], 'label': obj['Group'],
'children': [{
'value': server_id,
'label': obj['Name']}]}
else:
return internal_server_error(
_('The specified file is not in the correct format.'))
for item in groups:
all_servers.append(groups[item])
except Exception as e:
return internal_server_error(
_('Unable to load the specified file.'))
else:
return internal_server_error(_('The specified file does not exist.'))
return make_json_response(success=1, data=all_servers)
@blueprint.route('/save', methods=['POST'], endpoint='save')
@login_required
def save():
"""
This function is used to import or export based on the data
"""
required_args = [
'type', 'filename'
]
data = request.form if request.form else json.loads(request.data.decode())
for arg in required_args:
if arg not in data:
return make_json_response(
status=410,
success=0,
errormsg=_(
"Could not find the required parameter ({})."
).format(arg)
)
status = False
errmsg = None
summary_data = []
if data['type'] == 'export':
status, errmsg, summary_data = \
dump_database_servers(data['filename'], data['selected_sever_ids'])
elif data['type'] == 'import':
# Clear all the existing servers
if 'replace_servers' in data and data['replace_servers']:
clear_database_servers()
status, errmsg, summary_data = \
load_database_servers(data['filename'], data['selected_sever_ids'])
if not status:
return internal_server_error(errmsg)
return make_json_response(success=1, data=summary_data)

View File

@ -0,0 +1,259 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2021, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import gettext from 'sources/gettext';
import _ from 'lodash';
import url_for from 'sources/url_for';
import React from 'react';
import { Box, Paper} from '@material-ui/core';
import { makeStyles } from '@material-ui/core/styles';
import Wizard from '../../../../static/js/helpers/wizard/Wizard';
import WizardStep from '../../../../static/js/helpers/wizard/WizardStep';
import { FormFooterMessage, MESSAGE_TYPE } from '../../../../static/js/components/FormComponents';
import SchemaView from '../../../../static/js/SchemaView';
import Loader from 'sources/components/Loader';
import ImportExportSelectionSchema from './import_export_selection.ui';
import CheckBoxTree from '../../../../static/js/components/CheckBoxTree';
import getApiInstance from '../../../../static/js/api_instance';
import Alertify from 'pgadmin.alertifyjs';
import { commonTableStyles } from '../../../../static/js/Theme';
import clsx from 'clsx';
import Notify from '../../../../static/js/helpers/Notifier';
import pgAdmin from 'sources/pgadmin';
const useStyles = makeStyles(() =>
({
root: {
height: '100%'
},
treeContainer: {
flexGrow: 1,
minHeight: 0,
},
boxText: {
paddingBottom: '5px'
},
noOverflow: {
overflow: 'hidden'
},
summaryContainer: {
flexGrow: 1,
minHeight: 0,
overflow: 'auto',
}
}),
);
export default function ImportExportServers() {
const classes = useStyles();
const tableClasses = commonTableStyles();
var steps = ['Import/Export', 'Database Servers', 'Summary'];
const [loaderText, setLoaderText] = React.useState('');
const [errMsg, setErrMsg] = React.useState('');
const [selectionFormData, setSelectionFormData] = React.useState({});
const [serverData, setServerData] = React.useState([]);
const [selectedServers, setSelectedServers] = React.useState([]);
const [summaryData, setSummaryData] = React.useState([]);
const [summaryText, setSummaryText] = React.useState('');
const api = getApiInstance();
const onSave = () => {
if (selectionFormData.imp_exp == 'i') {
Notify.confirm(
gettext('Browser tree refresh required'),
gettext('A browser tree refresh is required. Do you wish to refresh the tree?'),
function() {
pgAdmin.Browser.tree.destroy({
success: function() {
pgAdmin.Browser.initializeBrowserTree(pgAdmin.Browser);
return true;
},
});
},
function() {
return true;
},
gettext('Refresh'),
gettext('Later')
);
}
Alertify.importExportWizardDialog().close();
};
const disableNextCheck = (stepId) => {
if (stepId == 0) {
return _.isEmpty(selectionFormData.filename);
} else if (stepId == 1) {
return selectedServers.length < 1;
}
return false;
};
const onDialogHelp= () => {
window.open(url_for('help.static', { 'filename': 'import_export_servers.html' }), 'pgadmin_help');
};
const onErrClose = React.useCallback(()=>{
setErrMsg('');
});
const wizardStepChange= (data) => {
switch (data.currentStep) {
case 2: {
let post_data = {'filename': selectionFormData.filename},
save_url = url_for('import_export_servers.save');
if (selectionFormData.imp_exp == 'e') {
setLoaderText('Exporting Server Groups/Servers ...');
setSummaryText('Exported following Server Groups/Servers:');
post_data['type'] = 'export';
post_data['selected_sever_ids'] = selectedServers;
api.post(save_url, post_data)
.then(res => {
setLoaderText('');
setSummaryData(res.data.data);
})
.catch((err) => {
setLoaderText('');
setErrMsg(err.response.data.errormsg);
});
} else if (selectionFormData.imp_exp == 'i') {
setLoaderText('Importing Server Groups/Servers ...');
setSummaryText('Imported following Server Groups/Servers:');
// Remove the random number added to create unique tree item,
let selected_sever_ids = [];
selectedServers.forEach((id) => {
selected_sever_ids.push(id.split('_')[0]);
});
post_data['type'] = 'import';
post_data['selected_sever_ids'] = selected_sever_ids;
post_data['replace_servers'] = selectionFormData.replace_servers;
api.post(save_url, post_data)
.then(res => {
setLoaderText('');
setSummaryData(res.data.data);
})
.catch((err) => {
setLoaderText('');
setErrMsg(err.response.data.errormsg);
});
}
break;
}
default:
break;
}
};
const onBeforeNext = (activeStep)=>{
return new Promise((resolve, reject)=>{
if(activeStep == 0) {
setLoaderText('Loading Servers/Server Groups ...');
if (selectionFormData.imp_exp == 'e') {
var get_servers_url = url_for('import_export_servers.get_servers');
api.get(get_servers_url)
.then(res => {
setLoaderText('');
setServerData(res.data.data);
resolve();
})
.catch(() => {
setLoaderText('');
setErrMsg(gettext('Error while fetching Server Groups and Servers.'));
reject();
});
} else if (selectionFormData.imp_exp == 'i') {
var load_servers_url = url_for('import_export_servers.load_servers');
const post_data = {
filename: selectionFormData.filename
};
api.post(load_servers_url, post_data)
.then(res => {
setLoaderText('');
setServerData(res.data.data);
resolve();
})
.catch((err) => {
setLoaderText('');
setErrMsg(err.response.data.errormsg);
reject();
});
}
} else {
resolve();
}
});
};
return (
<Box className={classes.root}>
<Loader message={loaderText} />
<Wizard
title={gettext('Import/Export Servers')}
stepList={steps}
disableNextStep={disableNextCheck}
onStepChange={wizardStepChange}
onSave={onSave}
onHelp={onDialogHelp}
beforeNext={onBeforeNext}
>
<WizardStep stepId={0}>
<SchemaView
formType={'dialog'}
getInitData={() => { }}
viewHelperProps={{ mode: 'create' }}
schema={new ImportExportSelectionSchema()}
showFooter={false}
isTabView={false}
onDataChange={(isChanged, changedData) => {
setSelectionFormData(changedData);
}}
/>
<FormFooterMessage type={MESSAGE_TYPE.ERROR} message={errMsg} onClose={onErrClose} />
</WizardStep>
<WizardStep stepId={1} className={classes.noOverflow}>
<Box className={classes.boxText}>{gettext('Select the Server Groups/Servers to import/export:')}</Box>
<Box className={classes.treeContainer}>
<CheckBoxTree treeData={serverData} getSelectedServers={(selectedServers) => {
setSelectedServers(selectedServers);
}}/>
</Box>
</WizardStep>
<WizardStep stepId={2} className={classes.noOverflow}>
<Box className={classes.boxText}>{gettext(summaryText)}</Box>
<Paper variant="outlined" elevation={0} className={classes.summaryContainer}>
<table className={clsx(tableClasses.table)}>
<thead>
<tr>
<th>Server Group</th>
<th>Server</th>
</tr>
</thead>
<tbody>
{summaryData.map((row) => (
<tr key={row.srno}>
<td>
{row.server_group}
</td>
<td>{row.server}</td>
</tr>
))}
</tbody>
</table>
</Paper>
</WizardStep>
</Wizard>
</Box>
);
}

View File

@ -0,0 +1,98 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2021, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import gettext from 'sources/gettext';
import BaseUISchema from 'sources/SchemaView/base_schema.ui';
import { isEmptyString } from 'sources/validators';
export default class ImportExportSelectionSchema extends BaseUISchema {
constructor(initData = {}) {
super({
imp_exp: 'i',
filename: undefined,
replace_servers: false,
...initData
});
}
get idAttribute() {
return 'id';
}
get baseFields() {
return [{
id: 'imp_exp',
label: gettext('Import/Export'),
type: 'toggle',
options: [
{'label': gettext('Import'), 'value': 'i'},
{'label': gettext('Export'), 'value': 'e'},
]
}, {
id: 'filename',
label: gettext('Filename'),
type: (state)=>{
if (state.imp_exp == 'e') {
return {
type: 'file',
controlProps: {
dialogType: 'create_file',
supportedTypes: ['json'],
dialogTitle: 'Create file',
},
};
}
return {
type: 'file',
controlProps: {
dialogType: 'select_file',
supportedTypes: ['json'],
dialogTitle: 'Select file',
},
};
},
deps: ['imp_exp'],
depChange: (state, source, topState, actionObj)=> {
if (state.imp_exp != actionObj.oldState.imp_exp) {
state.filename = undefined;
}
},
helpMessage: gettext('Supports only JSON format.')
}, {
id: 'replace_servers',
label: gettext('Replace existing servers?'),
type: 'switch', deps: ['imp_exp'],
depChange: (state)=> {
if (state.imp_exp == 'e') {
state.replace_servers = false;
}
},
disabled: function (state) {
if (state.imp_exp == 'e') {
return true;
}
return false;
}
}];
}
validate(state, setError) {
if (isEmptyString(state.service)) {
let errmsg = null;
/* events validation*/
if (!state.filename) {
errmsg = gettext('Please provide a filename.');
setError('filename', errmsg);
return true;
} else {
errmsg = null;
setError('filename', errmsg);
}
}
}
}

View File

@ -0,0 +1,124 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2021, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
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';
import ImportExportServers from './ImportExportServers';
import $ from 'jquery';
export default class ImportExportServersModule {
static instance;
static getInstance(...args) {
if(!ImportExportServersModule.instance) {
ImportExportServersModule.instance = new ImportExportServersModule(...args);
}
return ImportExportServersModule.instance;
}
constructor(pgAdmin, pgBrowser) {
this.pgAdmin = pgAdmin;
this.pgBrowser = pgBrowser;
}
init() {
if (this.initialized)
return;
this.initialized = true;
// Define the nodes on which the menus to be appear
var menus = [{
name: 'import_export_servers',
module: this,
applies: ['tools'],
callback: 'showImportExportServers',
enable: true,
priority: 3,
label: gettext('Import/Export Servers...'),
icon: 'fa fa-shopping-cart',
}];
this.pgBrowser.add_menus(menus);
}
// This is a callback function to show import/export servers when user click on menu item.
showImportExportServers() {
// Declare Wizard dialog
if (!Alertify.importExportWizardDialog) {
Alertify.dialog('importExportWizardDialog', function factory() {
// Generate wizard main container
var $container = $('<div class=\'wizard_dlg\' id=\'importExportServersDlg\'></div>');
return {
main: function () {
},
setup: function () {
return {
// Set options for dialog
options: {
frameless: true,
resizable: true,
autoReset: false,
maximizable: true,
closable: true,
closableByDimmer: false,
modal: true,
pinnable: false,
},
};
},
build: function () {
this.elements.content.appendChild($container.get(0));
Alertify.pgDialogBuild.apply(this);
setTimeout(function () {
if (document.getElementById('importExportServersDlg')) {
ReactDOM.render(
<Theme>
<ImportExportServers />
</Theme>,
document.getElementById('importExportServersDlg'));
Alertify.importExportWizardDialog().elements.modal.style.maxHeight=0;
Alertify.importExportWizardDialog().elements.modal.style.maxWidth='none';
Alertify.importExportWizardDialog().elements.modal.style.overflow='visible';
Alertify.importExportWizardDialog().elements.dimmer.style.display='none';
}
}, 500);
},
prepare: function () {
$container.empty().append('<div class=\'import_export_servers_container\'></div>');
},
hooks: {
// Triggered when the dialog is closed
onclose: function () {
// Clear the view and remove the react component.
return setTimeout((function () {
ReactDOM.unmountComponentAtNode(document.getElementById('importExportServersDlg'));
return Alertify.importExportWizardDialog().destroy();
}), 500);
},
}
};
});
}
Alertify.importExportWizardDialog('').set({
onmaximize:function(){
Alertify.importExportWizardDialog().elements.modal.style.maxHeight='initial';
},
onrestore:function(){
Alertify.importExportWizardDialog().elements.modal.style.maxHeight=0;
},
}).resizeTo(880, 550);
}
}

View File

@ -0,0 +1,20 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2021, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import pgAdmin from 'sources/pgadmin';
import pgBrowser from 'top/browser/static/js/browser';
import ImportExportServersModule from './import_export_servers';
if(!pgAdmin.Tools) {
pgAdmin.Tools = {};
}
pgAdmin.Tools.ImportExportServersModule = ImportExportServersModule.getInstance(pgAdmin, pgBrowser);
module.exports = {
ImportExportServersModule: ImportExportServersModule,
};

View File

@ -9,6 +9,7 @@
import os
import sys
import json
import subprocess
from collections import defaultdict
from operator import attrgetter
@ -20,8 +21,11 @@ from threading import Lock
from .paths import get_storage_directory
from .preferences import Preferences
from pgadmin.model import Server
from pgadmin.utils.constants import UTILITIES_ARRAY
from pgadmin.utils.constants import UTILITIES_ARRAY, USER_NOT_FOUND
from pgadmin.model import db, User, Version, ServerGroup, Server, \
SCHEMA_VERSION as CURRENT_SCHEMA_VERSION
ADD_SERVERS_MSG = "Added %d Server Group(s) and %d Server(s)."
class PgAdminModule(Blueprint):
@ -370,6 +374,368 @@ def replace_binary_path(binary_path):
return binary_path
def add_value(attr_dict, key, value):
"""Add a value to the attribute dict if non-empty.
Args:
attr_dict (dict): The dictionary to add the values to
key (str): The key for the new value
value (str): The value to add
Returns:
The updated attribute dictionary
"""
if value != "" and value is not None:
attr_dict[key] = value
return attr_dict
def dump_database_servers(output_file, selected_servers,
dump_user=current_user, from_setup=False):
"""Dump the server groups and servers.
"""
user = _does_user_exist(dump_user, from_setup)
if user is None:
return False, USER_NOT_FOUND % dump_user, []
user_id = user.id
# Dict to collect the output
object_dict = {}
# Counters
servers_dumped = 0
# Dump servers
servers = Server.query.filter_by(user_id=user_id).all()
server_dict = {}
dump_servers = []
for server in servers:
if selected_servers is None or str(server.id) in selected_servers:
# Get the group name
group_name = ServerGroup.query.filter_by(
user_id=user_id, id=server.servergroup_id).first().name
attr_dict = {}
add_value(attr_dict, "Name", server.name)
add_value(attr_dict, "Group", group_name)
add_value(attr_dict, "Host", server.host)
add_value(attr_dict, "HostAddr", server.hostaddr)
add_value(attr_dict, "Port", server.port)
add_value(attr_dict, "MaintenanceDB", server.maintenance_db)
add_value(attr_dict, "Username", server.username)
add_value(attr_dict, "Role", server.role)
add_value(attr_dict, "SSLMode", server.ssl_mode)
add_value(attr_dict, "Comment", server.comment)
add_value(attr_dict, "Shared", server.shared)
add_value(attr_dict, "DBRestriction", server.db_res)
add_value(attr_dict, "PassFile", server.passfile)
add_value(attr_dict, "SSLCert", server.sslcert)
add_value(attr_dict, "SSLKey", server.sslkey)
add_value(attr_dict, "SSLRootCert", server.sslrootcert)
add_value(attr_dict, "SSLCrl", server.sslcrl)
add_value(attr_dict, "SSLCompression", server.sslcompression)
add_value(attr_dict, "BGColor", server.bgcolor)
add_value(attr_dict, "FGColor", server.fgcolor)
add_value(attr_dict, "Service", server.service)
add_value(attr_dict, "Timeout", server.connect_timeout)
add_value(attr_dict, "UseSSHTunnel", server.use_ssh_tunnel)
add_value(attr_dict, "TunnelHost", server.tunnel_host)
add_value(attr_dict, "TunnelPort", server.tunnel_port)
add_value(attr_dict, "TunnelUsername", server.tunnel_username)
add_value(attr_dict, "TunnelAuthentication",
server.tunnel_authentication)
servers_dumped = servers_dumped + 1
dump_servers.append({'srno': servers_dumped,
'server_group': group_name,
'server': server.name})
server_dict[servers_dumped] = attr_dict
object_dict["Servers"] = server_dict
f = None
try:
f = open(output_file, "w")
except Exception as e:
return _handle_error("Error opening output file %s: [%d] %s" %
(output_file, e.errno, e.strerror), from_setup)
try:
f.write(json.dumps(object_dict, indent=4))
except Exception as e:
return _handle_error("Error writing output file %s: [%d] %s" %
(output_file, e.errno, e.strerror), from_setup)
f.close()
msg = "Configuration for %s servers dumped to %s." % \
(servers_dumped, output_file)
print(msg)
return True, msg, dump_servers
def _validate_servers_data(data, is_admin):
"""
Used internally by load_servers to validate servers data.
:param data: servers data
:return: error message if any
"""
skip_servers = []
# Loop through the servers...
if "Servers" not in data:
return "'Servers' attribute not found in the specified file."
for server in data["Servers"]:
obj = data["Servers"][server]
# Check if server is shared.Won't import if user is non-admin
if obj.get('Shared', None) and not is_admin:
print("Won't import the server '%s' as it is shared " %
obj["Name"])
skip_servers.append(server)
continue
def check_attrib(attrib):
if attrib not in obj:
return ("'%s' attribute not found for server '%s'" %
(attrib, server))
return None
for attrib in ("Group", "Name"):
errmsg = check_attrib(attrib)
if errmsg:
return errmsg
is_service_attrib_available = obj.get("Service", None) is not None
if not is_service_attrib_available:
for attrib in ("Port", "Username"):
errmsg = check_attrib(attrib)
if errmsg:
return errmsg
for attrib in ("SSLMode", "MaintenanceDB"):
errmsg = check_attrib(attrib)
if errmsg:
return errmsg
if "Host" not in obj and "HostAddr" not in obj and not \
is_service_attrib_available:
return ("'Host', 'HostAddr' or 'Service' attribute "
"not found for server '%s'" % server)
for server in skip_servers:
del data["Servers"][server]
return None
def load_database_servers(input_file, selected_servers,
load_user=current_user, from_setup=False):
"""Load server groups and servers.
"""
try:
with open(input_file) as f:
data = json.load(f)
except json.decoder.JSONDecodeError as e:
return _handle_error("Error parsing input file %s: %s" %
(input_file, e), from_setup)
except Exception as e:
return _handle_error("Error reading input file %s: [%d] %s" %
(input_file, e.errno, e.strerror), from_setup)
f.close()
user = _does_user_exist(load_user, from_setup)
if user is None:
return False, USER_NOT_FOUND % load_user, []
user_id = user.id
# Counters
groups_added = 0
servers_added = 0
# Get the server groups
groups = ServerGroup.query.filter_by(user_id=user_id)
# Validate server data
error_msg = _validate_servers_data(data, user.has_role("Administrator"))
if error_msg is not None and from_setup:
print(ADD_SERVERS_MSG % (groups_added, servers_added))
return _handle_error(error_msg, from_setup)
load_servers = []
for server in data["Servers"]:
if selected_servers is None or str(server) in selected_servers:
obj = data["Servers"][server]
# Get the group. Create if necessary
group_id = next(
(g.id for g in groups if g.name == obj["Group"]), -1)
if group_id == -1:
new_group = ServerGroup()
new_group.name = obj["Group"]
new_group.user_id = user_id
db.session.add(new_group)
try:
db.session.commit()
except Exception as e:
if from_setup:
print(ADD_SERVERS_MSG % (groups_added, servers_added))
return _handle_error(
"Error creating server group '%s': %s" %
(new_group.name, e), from_setup)
group_id = new_group.id
groups_added = groups_added + 1
groups = ServerGroup.query.filter_by(user_id=user_id)
# Create the server
new_server = Server()
new_server.name = obj["Name"]
new_server.servergroup_id = group_id
new_server.user_id = user_id
new_server.ssl_mode = obj["SSLMode"]
new_server.maintenance_db = obj["MaintenanceDB"]
new_server.host = obj.get("Host", None)
new_server.hostaddr = obj.get("HostAddr", None)
new_server.port = obj.get("Port", None)
new_server.username = obj.get("Username", None)
new_server.role = obj.get("Role", None)
new_server.ssl_mode = obj["SSLMode"]
new_server.comment = obj.get("Comment", None)
new_server.db_res = obj.get("DBRestriction", None)
new_server.passfile = obj.get("PassFile", None)
new_server.sslcert = obj.get("SSLCert", None)
new_server.sslkey = obj.get("SSLKey", None)
new_server.sslrootcert = obj.get("SSLRootCert", None)
new_server.sslcrl = obj.get("SSLCrl", None)
new_server.sslcompression = obj.get("SSLCompression", None)
new_server.bgcolor = obj.get("BGColor", None)
new_server.fgcolor = obj.get("FGColor", None)
new_server.service = obj.get("Service", None)
new_server.connect_timeout = obj.get("Timeout", None)
new_server.use_ssh_tunnel = obj.get("UseSSHTunnel", None)
new_server.tunnel_host = obj.get("TunnelHost", None)
new_server.tunnel_port = obj.get("TunnelPort", None)
new_server.tunnel_username = obj.get("TunnelUsername", None)
new_server.tunnel_authentication = \
obj.get("TunnelAuthentication", None)
new_server.shared = \
obj.get("Shared", None)
db.session.add(new_server)
try:
db.session.commit()
except Exception as e:
if from_setup:
print(ADD_SERVERS_MSG % (groups_added, servers_added))
return _handle_error("Error creating server '%s': %s" %
(new_server.name, e), from_setup)
servers_added = servers_added + 1
load_servers.append({'srno': servers_added,
'server_group': obj["Group"],
'server': obj["Name"]})
msg = ADD_SERVERS_MSG % (groups_added, servers_added)
print(msg)
return True, msg, load_servers
def clear_database_servers(load_user=current_user, from_setup=False):
"""Clear groups and servers configurations.
"""
user = _does_user_exist(load_user, from_setup)
if user is None:
return False
user_id = user.id
# Remove all servers
servers = Server.query.filter_by(user_id=user_id)
for server in servers:
db.session.delete(server)
# Remove all groups
groups = ServerGroup.query.filter_by(user_id=user_id)
for group in groups:
db.session.delete(group)
servers = Server.query.filter_by(user_id=user_id)
for server in servers:
db.session.delete(server)
try:
db.session.commit()
except Exception as e:
error_msg = "Error clearing server configuration with error (%s)" % \
str(e)
if from_setup:
print(error_msg)
sys.exit(1)
return False, error_msg
def _does_user_exist(user, from_setup):
"""
This function will check user is exist or not. If exist then return
"""
if isinstance(user, User):
user = user.email
user = User.query.filter_by(email=user).first()
if user is None:
print(USER_NOT_FOUND % user)
if from_setup:
sys.exit(1)
return user
def _handle_error(error_msg, from_setup):
"""
This function is used to print the error msg and exit from app if
called from setup.py
"""
if from_setup:
print(error_msg)
sys.exit(1)
return False, error_msg, []
# Shortcut configuration for Accesskey
ACCESSKEY_FIELDS = [
{

View File

@ -110,3 +110,4 @@ BINARY_PATHS = {
UTILITIES_ARRAY = ['pg_dump', 'pg_dumpall', 'pg_restore', 'psql']
ENTER_EMAIL_ADDRESS = "Email address: "
USER_NOT_FOUND = gettext("The specified user ID (%s) could not be found.")

View File

@ -0,0 +1,68 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2021, 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 pgAdmin from 'sources/pgadmin';
import { messages } from '../fake_messages';
import SchemaView from '../../../pgadmin/static/js/SchemaView';
import ImportExportSelectionSchema from '../../../pgadmin/tools/import_export_servers/static/js/import_export_selection.ui';
describe('ImportExportServers', () => {
let mount;
let schemaObj = new ImportExportSelectionSchema();
/* 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();
/* messages used by validators */
pgAdmin.Browser = pgAdmin.Browser || {};
pgAdmin.Browser.messages = pgAdmin.Browser.messages || messages;
pgAdmin.Browser.utils = pgAdmin.Browser.utils || {};
});
it('import', () => {
mount(<SchemaView
formType='dialog'
schema={schemaObj}
viewHelperProps={{
mode: 'create',
}}
onDataChange={() => { }}
showFooter={false}
isTabView={false}
/>);
});
it('export', () => {
let schemaObj = new ImportExportSelectionSchema(
{imp_exp: 'e', filename: 'test.json'});
mount(<SchemaView
formType='dialog'
schema={schemaObj}
viewHelperProps={{
mode: 'create',
}}
onDataChange={() => { }}
showFooter={false}
isTabView={false}
/>);
});
});

View File

@ -16,8 +16,6 @@ import os
import sys
import builtins
USER_NOT_FOUND = "The specified user ID (%s) could not be found."
# Grab the SERVER_MODE if it's been set by the runtime
if 'SERVER_MODE' in globals():
builtins.SERVER_MODE = globals()['SERVER_MODE']
@ -30,26 +28,10 @@ root = os.path.dirname(os.path.realpath(__file__))
if sys.path[0] != root:
sys.path.insert(0, root)
from pgadmin.model import db, User, Version, ServerGroup, Server, \
SCHEMA_VERSION as CURRENT_SCHEMA_VERSION
from pgadmin.model import db, Version, SCHEMA_VERSION as CURRENT_SCHEMA_VERSION
from pgadmin import create_app
def add_value(attr_dict, key, value):
"""Add a value to the attribute dict if non-empty.
Args:
attr_dict (dict): The dictionary to add the values to
key (str): The key for the new value
value (str): The value to add
Returns:
The updated attribute dictionary
"""
if value != "" and value is not None:
attr_dict[key] = value
return attr_dict
from pgadmin.utils import clear_database_servers, dump_database_servers,\
load_database_servers
def dump_servers(args):
@ -77,139 +59,7 @@ def dump_servers(args):
app = create_app(config.APP_NAME + '-cli')
with app.app_context():
user = User.query.filter_by(email=dump_user).first()
if user is None:
print(USER_NOT_FOUND % dump_user)
sys.exit(1)
user_id = user.id
# Dict to collect the output
object_dict = {}
# Counters
servers_dumped = 0
# Dump servers
servers = Server.query.filter_by(user_id=user_id).all()
server_dict = {}
for server in servers:
if args.servers is None or str(server.id) in args.servers:
# Get the group name
group_name = ServerGroup.query.filter_by(
user_id=user_id, id=server.servergroup_id).first().name
attr_dict = {}
add_value(attr_dict, "Name", server.name)
add_value(attr_dict, "Group", group_name)
add_value(attr_dict, "Host", server.host)
add_value(attr_dict, "HostAddr", server.hostaddr)
add_value(attr_dict, "Port", server.port)
add_value(attr_dict, "MaintenanceDB", server.maintenance_db)
add_value(attr_dict, "Username", server.username)
add_value(attr_dict, "Role", server.role)
add_value(attr_dict, "SSLMode", server.ssl_mode)
add_value(attr_dict, "Comment", server.comment)
add_value(attr_dict, "Shared", server.shared)
add_value(attr_dict, "DBRestriction", server.db_res)
add_value(attr_dict, "PassFile", server.passfile)
add_value(attr_dict, "SSLCert", server.sslcert)
add_value(attr_dict, "SSLKey", server.sslkey)
add_value(attr_dict, "SSLRootCert", server.sslrootcert)
add_value(attr_dict, "SSLCrl", server.sslcrl)
add_value(attr_dict, "SSLCompression", server.sslcompression)
add_value(attr_dict, "BGColor", server.bgcolor)
add_value(attr_dict, "FGColor", server.fgcolor)
add_value(attr_dict, "Service", server.service)
add_value(attr_dict, "Timeout", server.connect_timeout)
add_value(attr_dict, "UseSSHTunnel", server.use_ssh_tunnel)
add_value(attr_dict, "TunnelHost", server.tunnel_host)
add_value(attr_dict, "TunnelPort", server.tunnel_port)
add_value(attr_dict, "TunnelUsername", server.tunnel_username)
add_value(attr_dict, "TunnelAuthentication",
server.tunnel_authentication)
servers_dumped = servers_dumped + 1
server_dict[servers_dumped] = attr_dict
object_dict["Servers"] = server_dict
try:
f = open(args.dump_servers, "w")
except Exception as e:
print("Error opening output file %s: [%d] %s" %
(args.dump_servers, e.errno, e.strerror))
sys.exit(1)
try:
f.write(json.dumps(object_dict, indent=4))
except Exception as e:
print("Error writing output file %s: [%d] %s" %
(args.dump_servers, e.errno, e.strerror))
sys.exit(1)
f.close()
print("Configuration for %s servers dumped to %s." %
(servers_dumped, args.dump_servers))
def _validate_servers_data(data, is_admin):
"""
Used internally by load_servers to validate servers data.
:param data: servers data
:return: error message if any
"""
skip_servers = []
# Loop through the servers...
if "Servers" not in data:
return ("'Servers' attribute not found in file '%s'" %
args.load_servers)
for server in data["Servers"]:
obj = data["Servers"][server]
# Check if server is shared.Won't import if user is non-admin
if obj.get('Shared', None) and not is_admin:
print("Won't import the server '%s' as it is shared " %
obj["Name"])
skip_servers.append(server)
continue
def check_attrib(attrib):
if attrib not in obj:
return ("'%s' attribute not found for server '%s'" %
(attrib, server))
return None
for attrib in ("Group", "Name"):
errmsg = check_attrib(attrib)
if errmsg:
return errmsg
is_service_attrib_available = obj.get("Service", None) is not None
if not is_service_attrib_available:
for attrib in ("Port", "Username"):
errmsg = check_attrib(attrib)
if errmsg:
return errmsg
for attrib in ("SSLMode", "MaintenanceDB"):
errmsg = check_attrib(attrib)
if errmsg:
return errmsg
if "Host" not in obj and "HostAddr" not in obj and not \
is_service_attrib_available:
return ("'Host', 'HostAddr' or 'Service' attribute "
"not found for server '%s'" % server)
for server in skip_servers:
del data["Servers"][server]
return None
dump_database_servers(args.dump_servers, args.servers, dump_user, True)
def load_servers(args):
@ -232,143 +82,9 @@ def load_servers(args):
print('SQLite pgAdmin config:', config.SQLITE_PATH)
print('----------')
try:
with open(args.load_servers) as f:
data = json.load(f)
except json.decoder.JSONDecodeError as e:
print("Error parsing input file %s: %s" %
(args.load_servers, e))
sys.exit(1)
except Exception as e:
print("Error reading input file %s: [%d] %s" %
(args.load_servers, e.errno, e.strerror))
sys.exit(1)
f.close()
app = create_app(config.APP_NAME + '-cli')
with app.app_context():
user = User.query.filter_by(email=load_user).first()
if user is None:
print(USER_NOT_FOUND % load_user)
sys.exit(1)
user_id = user.id
# Counters
groups_added = 0
servers_added = 0
# Get the server groups
groups = ServerGroup.query.filter_by(user_id=user_id)
def print_summary():
print("Added %d Server Group(s) and %d Server(s)." %
(groups_added, servers_added))
err_msg = _validate_servers_data(data, user.has_role("Administrator"))
if err_msg is not None:
print(err_msg)
print_summary()
sys.exit(1)
for server in data["Servers"]:
obj = data["Servers"][server]
# Get the group. Create if necessary
group_id = next(
(g.id for g in groups if g.name == obj["Group"]), -1)
if group_id == -1:
new_group = ServerGroup()
new_group.name = obj["Group"]
new_group.user_id = user_id
db.session.add(new_group)
try:
db.session.commit()
except Exception as e:
print("Error creating server group '%s': %s" %
(new_group.name, e))
print_summary()
sys.exit(1)
group_id = new_group.id
groups_added = groups_added + 1
groups = ServerGroup.query.filter_by(user_id=user_id)
# Create the server
new_server = Server()
new_server.name = obj["Name"]
new_server.servergroup_id = group_id
new_server.user_id = user_id
new_server.ssl_mode = obj["SSLMode"]
new_server.maintenance_db = obj["MaintenanceDB"]
new_server.host = obj.get("Host", None)
new_server.hostaddr = obj.get("HostAddr", None)
new_server.port = obj.get("Port", None)
new_server.username = obj.get("Username", None)
new_server.role = obj.get("Role", None)
new_server.ssl_mode = obj["SSLMode"]
new_server.comment = obj.get("Comment", None)
new_server.db_res = obj.get("DBRestriction", None)
new_server.passfile = obj.get("PassFile", None)
new_server.sslcert = obj.get("SSLCert", None)
new_server.sslkey = obj.get("SSLKey", None)
new_server.sslrootcert = obj.get("SSLRootCert", None)
new_server.sslcrl = obj.get("SSLCrl", None)
new_server.sslcompression = obj.get("SSLCompression", None)
new_server.bgcolor = obj.get("BGColor", None)
new_server.fgcolor = obj.get("FGColor", None)
new_server.service = obj.get("Service", None)
new_server.connect_timeout = obj.get("Timeout", None)
new_server.use_ssh_tunnel = obj.get("UseSSHTunnel", None)
new_server.tunnel_host = obj.get("TunnelHost", None)
new_server.tunnel_port = obj.get("TunnelPort", None)
new_server.tunnel_username = obj.get("TunnelUsername", None)
new_server.tunnel_authentication = \
obj.get("TunnelAuthentication", None)
new_server.shared = \
obj.get("Shared", None)
db.session.add(new_server)
try:
db.session.commit()
except Exception as e:
print("Error creating server '%s': %s" %
(new_server.name, e))
print_summary()
sys.exit(1)
servers_added = servers_added + 1
print_summary()
load_database_servers(args.load_servers, None, load_user, True)
def setup_db():
@ -421,33 +137,7 @@ def clear_servers():
app = create_app(config.APP_NAME + '-cli')
with app.app_context():
user = User.query.filter_by(email=load_user).first()
if user is None:
print(USER_NOT_FOUND % load_user)
sys.exit(1)
user_id = user.id
# Remove all servers
servers = Server.query.filter_by(user_id=user_id)
for server in servers:
db.session.delete(server)
# Remove all groups
groups = ServerGroup.query.filter_by(user_id=user_id)
for group in groups:
db.session.delete(group)
servers = Server.query.filter_by(user_id=user_id)
for server in servers:
db.session.delete(server)
try:
db.session.commit()
except Exception as e:
print("Error clearing server configuration with error (%s)" %
str(e))
clear_database_servers(load_user, True)
if __name__ == '__main__':

View File

@ -543,6 +543,7 @@ module.exports = [{
'pure|pgadmin.tools.grant_wizard',
'pure|pgadmin.tools.maintenance',
'pure|pgadmin.tools.import_export',
'pure|pgadmin.tools.import_export_servers',
'pure|pgadmin.tools.debugger.controller',
'pure|pgadmin.tools.debugger.direct',
'pure|pgadmin.node.pga_job',

View File

@ -295,6 +295,7 @@ var webpackShimConfig = {
'pgadmin.tools.debugger.utils': path.join(__dirname, './pgadmin/tools/debugger/static/js/debugger_utils'),
'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/'),
'pgadmin.tools.maintenance': path.join(__dirname, './pgadmin/tools/maintenance/static/js/maintenance'),
'pgadmin.tools.restore': path.join(__dirname, './pgadmin/tools/restore/static/js/restore'),
'pgadmin.tools.schema_diff': path.join(__dirname, './pgadmin/tools/schema_diff/static/js/schema_diff'),

View File

@ -2865,7 +2865,7 @@ circular-json-es6@^2.0.1:
resolved "https://registry.yarnpkg.com/circular-json-es6/-/circular-json-es6-2.0.2.tgz#e4f4a093e49fb4b6aba1157365746112a78bd344"
integrity sha512-ODYONMMNb3p658Zv+Pp+/XPa5s6q7afhz3Tzyvo+VRh9WIrJ64J76ZC4GQxnlye/NesTn09jvOiuE8+xxfpwhQ==
classnames@*, classnames@^2.2.6:
classnames@*, classnames@^2.2.5, classnames@^2.2.6:
version "2.3.1"
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e"
integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==
@ -6646,6 +6646,11 @@ nanocolors@^0.1.12:
resolved "https://registry.yarnpkg.com/nanocolors/-/nanocolors-0.1.12.tgz#8577482c58cbd7b5bb1681db4cf48f11a87fd5f6"
integrity sha512-2nMHqg1x5PU+unxX7PGY7AuYxl2qDx7PSrTRjizr8sxdd3l/3hBuWWaki62qmtYm2U5i4Z5E7GbjlyDFhs9/EQ==
nanoid@^3.0.0:
version "3.1.30"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.30.tgz#63f93cc548d2a113dc5dfbc63bfa09e2b9b64362"
integrity sha512-zJpuPDwOv8D2zq2WRoMe1HsfZthVewpel9CAvTfc/2mBD1uUT/agc5f7GHGWXlYkFvi1mVxe4IjvP2HNrop7nQ==
nanoid@^3.1.23:
version "3.1.23"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.23.tgz#f744086ce7c2bc47ee0a8472574d5c78e4183a81"
@ -7763,6 +7768,16 @@ react-aspen@^1.1.0, react-aspen@^1.1.1:
path-fx "^2.1.1"
react-window "^1.3.1"
react-checkbox-tree@^1.7.2:
version "1.7.2"
resolved "https://registry.yarnpkg.com/react-checkbox-tree/-/react-checkbox-tree-1.7.2.tgz#71cb5d22add293a92eb718de8425c8430b2db263"
integrity sha512-T0Y3Us2ds5QppOgIM/cSbtdrEBcCGkiz03o2p4elTireAIw0i5k5xPoaTxbjWTFmzgXajUrJzQMlBujEQhOUsQ==
dependencies:
classnames "^2.2.5"
lodash "^4.17.10"
nanoid "^3.0.0"
prop-types "^15.5.8"
react-dom@^16.6.3:
version "16.14.0"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.14.0.tgz#7ad838ec29a777fb3c75c3a190f661cf92ab8b89"