diff --git a/docs/en_US/images/import_export_servers_refresh_tree.png b/docs/en_US/images/import_export_servers_refresh_tree.png new file mode 100644 index 000000000..8f14e0d74 Binary files /dev/null and b/docs/en_US/images/import_export_servers_refresh_tree.png differ diff --git a/docs/en_US/images/import_export_servers_step1.png b/docs/en_US/images/import_export_servers_step1.png new file mode 100644 index 000000000..75b110269 Binary files /dev/null and b/docs/en_US/images/import_export_servers_step1.png differ diff --git a/docs/en_US/images/import_export_servers_step2.png b/docs/en_US/images/import_export_servers_step2.png new file mode 100644 index 000000000..91fdea906 Binary files /dev/null and b/docs/en_US/images/import_export_servers_step2.png differ diff --git a/docs/en_US/images/import_export_servers_step3.png b/docs/en_US/images/import_export_servers_step3.png new file mode 100644 index 000000000..f7e6ea8dc Binary files /dev/null and b/docs/en_US/images/import_export_servers_step3.png differ diff --git a/docs/en_US/import_export_servers.rst b/docs/en_US/import_export_servers.rst index 564472455..447c08bec 100644 --- a/docs/en_US/import_export_servers.rst +++ b/docs/en_US/import_export_servers.rst @@ -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 ***************** diff --git a/docs/en_US/release_notes_6_4.rst b/docs/en_US/release_notes_6_4.rst index 4ddd4ebfa..87a91c990 100644 --- a/docs/en_US/release_notes_6_4.rst +++ b/docs/en_US/release_notes_6_4.rst @@ -9,6 +9,7 @@ This release contains a number of bug fixes and new features since the release o New features ************ +| `Issue #4803 `_ - Added support to import/export server groups and servers from GUI. Housekeeping ************ @@ -21,7 +22,7 @@ Bug fixes | `Issue #6745 `_ - Fixed an issue where Tablespace is created though an error is shown on the dialog. | `Issue #7003 `_ - Fixed an issue where Explain Analyze shows negative exclusive time. | `Issue #7034 `_ - Fixed an issue where Columns with default value not showing when adding a new row. -| `Issue #7075 `_ - Ensure that help should be visible properly for Procedures. +| `Issue #7075 `_ - Ensure that help should be visible properly for Procedures. | `Issue #7077 `_ - Fixed an issue where the Owner is not displayed in the reverse engineering SQL for Procedures. | `Issue #7078 `_ - Fixed an issue where an operation error message pop up showing the database object's name incorrectly. | `Issue #7081 `_ - Fixed an issue in SQL generation for PostgreSQL-14 functions. diff --git a/docs/en_US/tree_control.rst b/docs/en_US/tree_control.rst index f5008d5db..433b89fdf 100644 --- a/docs/en_US/tree_control.rst +++ b/docs/en_US/tree_control.rst @@ -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... ` dialog to import data to or export data from the selected table. | +| *Import/Export Data...* | Click open the :ref:`Import/Export... ` dialog to import data to or export data from the selected table. | +-------------------------+------------------------------------------------------------------------------------------------------------------------------+ | *Reset Statistics* | Click to reset statistics for the selected table. | +-------------------------+------------------------------------------------------------------------------------------------------------------------------+ diff --git a/web/package.json b/web/package.json index 22aa90523..3d598a0c8 100644 --- a/web/package.json +++ b/web/package.json @@ -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", diff --git a/web/pgadmin/static/css/style.css b/web/pgadmin/static/css/style.css index 886eb5ee0..c8efae69c 100644 --- a/web/pgadmin/static/css/style.css +++ b/web/pgadmin/static/css/style.css @@ -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'; diff --git a/web/pgadmin/static/js/components/CheckBoxTree.jsx b/web/pgadmin/static/js/components/CheckBoxTree.jsx new file mode 100644 index 000000000..11d303056 --- /dev/null +++ b/web/pgadmin/static/js/components/CheckBoxTree.jsx @@ -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 ( +
+ setChecked(checked)} + onExpand={expanded => setExpanded(expanded)} + showNodeIcon={false} + icons={{ + check: , + uncheck: , + halfCheck: , + expandClose: , + expandOpen: , + leaf: + }} + /> +
+ ); +} + +CheckBoxTree.propTypes = { + treeData: PropTypes.array, + getSelectedServers: PropTypes.func +}; diff --git a/web/pgadmin/tools/import_export_servers/__init__.py b/web/pgadmin/tools/import_export_servers/__init__.py new file mode 100644 index 000000000..ff7b28720 --- /dev/null +++ b/web/pgadmin/tools/import_export_servers/__init__.py @@ -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) diff --git a/web/pgadmin/tools/import_export_servers/static/js/ImportExportServers.jsx b/web/pgadmin/tools/import_export_servers/static/js/ImportExportServers.jsx new file mode 100644 index 000000000..36843858a --- /dev/null +++ b/web/pgadmin/tools/import_export_servers/static/js/ImportExportServers.jsx @@ -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 ( + + + + + { }} + viewHelperProps={{ mode: 'create' }} + schema={new ImportExportSelectionSchema()} + showFooter={false} + isTabView={false} + onDataChange={(isChanged, changedData) => { + setSelectionFormData(changedData); + }} + /> + + + + {gettext('Select the Server Groups/Servers to import/export:')} + + { + setSelectedServers(selectedServers); + }}/> + + + + {gettext(summaryText)} + + + + + + + + + + {summaryData.map((row) => ( + + + + + ))} + +
Server GroupServer
+ {row.server_group} + {row.server}
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/web/pgadmin/tools/import_export_servers/static/js/import_export_selection.ui.js b/web/pgadmin/tools/import_export_servers/static/js/import_export_selection.ui.js new file mode 100644 index 000000000..1fdd3ec55 --- /dev/null +++ b/web/pgadmin/tools/import_export_servers/static/js/import_export_selection.ui.js @@ -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); + } + } + } +} diff --git a/web/pgadmin/tools/import_export_servers/static/js/import_export_servers.js b/web/pgadmin/tools/import_export_servers/static/js/import_export_servers.js new file mode 100644 index 000000000..005ab192f --- /dev/null +++ b/web/pgadmin/tools/import_export_servers/static/js/import_export_servers.js @@ -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 = $('
'); + 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( + + + , + 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('
'); + }, + 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); + } +} diff --git a/web/pgadmin/tools/import_export_servers/static/js/index.js b/web/pgadmin/tools/import_export_servers/static/js/index.js new file mode 100644 index 000000000..3cdc3ada0 --- /dev/null +++ b/web/pgadmin/tools/import_export_servers/static/js/index.js @@ -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, +}; diff --git a/web/pgadmin/utils/__init__.py b/web/pgadmin/utils/__init__.py index 2262eaf74..2705db61e 100644 --- a/web/pgadmin/utils/__init__.py +++ b/web/pgadmin/utils/__init__.py @@ -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 = [ { diff --git a/web/pgadmin/utils/constants.py b/web/pgadmin/utils/constants.py index 48b702c4c..3433ab222 100644 --- a/web/pgadmin/utils/constants.py +++ b/web/pgadmin/utils/constants.py @@ -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.") diff --git a/web/regression/javascript/schema_ui_files/import_export_servers.ui.spec.js b/web/regression/javascript/schema_ui_files/import_export_servers.ui.spec.js new file mode 100644 index 000000000..907cfbe02 --- /dev/null +++ b/web/regression/javascript/schema_ui_files/import_export_servers.ui.spec.js @@ -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( { }} + showFooter={false} + isTabView={false} + />); + }); + + it('export', () => { + let schemaObj = new ImportExportSelectionSchema( + {imp_exp: 'e', filename: 'test.json'}); + mount( { }} + showFooter={false} + isTabView={false} + />); + }); +}); diff --git a/web/setup.py b/web/setup.py index d2735cbc0..dd11990a3 100644 --- a/web/setup.py +++ b/web/setup.py @@ -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__': diff --git a/web/webpack.config.js b/web/webpack.config.js index 40bf62696..766b6ca9a 100644 --- a/web/webpack.config.js +++ b/web/webpack.config.js @@ -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', diff --git a/web/webpack.shim.js b/web/webpack.shim.js index 694bfe823..1e023be72 100644 --- a/web/webpack.shim.js +++ b/web/webpack.shim.js @@ -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'), diff --git a/web/yarn.lock b/web/yarn.lock index a156a8490..230485c4a 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -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"