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

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