Port connect server password dialog to React. Fixes #7337

This commit is contained in:
Akshay Joshi 2022-05-16 16:21:14 +05:30
parent 7686c33cbd
commit 44f9ba4a57
14 changed files with 205 additions and 218 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 46 KiB

View File

@ -13,6 +13,7 @@ New features
Housekeeping
************
| `Issue #7337 <https://redmine.postgresql.org/issues/7337>`_ - Port connect server password dialog to React.
Bug fixes
*********

View File

@ -1284,7 +1284,7 @@ class ServerNode(PGChildNodeView):
}
)
def connect(self, gid, sid, user_name=None, resp_json=False):
def connect(self, gid, sid):
"""
Connect the Server and return the connection object.
Verification Process before Connection:
@ -1410,8 +1410,7 @@ class ServerNode(PGChildNodeView):
# not provided, or password has not been saved earlier.
if prompt_password or prompt_tunnel_password:
return self.get_response_for_password(
server, 428, prompt_password, prompt_tunnel_password,
user=user_name, resp_json=resp_json
server, 428, prompt_password, prompt_tunnel_password
)
status = True
@ -1427,8 +1426,7 @@ class ServerNode(PGChildNodeView):
except Exception as e:
current_app.logger.exception(e)
return self.get_response_for_password(
server, 401, True, True, getattr(e, 'message', str(e)),
resp_json=resp_json)
server, 401, True, True, getattr(e, 'message', str(e)))
if not status:
@ -1440,7 +1438,7 @@ class ServerNode(PGChildNodeView):
return internal_server_error(errmsg)
return self.get_response_for_password(
server, 401, True, True, errmsg, resp_json=resp_json
server, 401, True, True, errmsg
)
else:
if save_password and config.ALLOW_SAVE_PASSWORD:
@ -1864,8 +1862,7 @@ class ServerNode(PGChildNodeView):
return internal_server_error(errormsg=str(e))
def get_response_for_password(self, server, status, prompt_password=False,
prompt_tunnel_password=False, errmsg=None,
user=None, resp_json=False):
prompt_tunnel_password=False, errmsg=None):
if server.use_ssh_tunnel:
data = {
@ -1888,11 +1885,7 @@ class ServerNode(PGChildNodeView):
return make_json_response(
success=0,
status=status,
result=render_template(
'servers/tunnel_password.html',
_=gettext,
**data,
) if not resp_json else data
result=data
)
else:
data = {
@ -1908,11 +1901,7 @@ class ServerNode(PGChildNodeView):
return make_json_response(
success=0,
status=status,
result=render_template(
'servers/password.html',
_=gettext,
**data
) if not resp_json else data
result=data
)
def clear_saved_password(self, gid, sid):

View File

@ -12,6 +12,7 @@ import { getNodePrivilegeRoleSchema } from '../../../static/js/privilege.ui';
import { getNodeVariableSchema } from '../../../static/js/variable.ui';
import DatabaseSchema from './database.ui';
import Notify from '../../../../../../static/js/helpers/Notifier';
import { showServerPassword } from '../../../../../static/js/password_dialogs';
define('pgadmin.node.database', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
@ -400,11 +401,11 @@ define('pgadmin.node.database', [
if (msg == 'CRYPTKEY_SET') {
connect_to_database(_model, _data, _tree, _item, _wasConnected);
} else {
Alertify.dlgServerPass(
showServerPassword(
gettext('Connect to database'),
msg, _model, _data, _tree, _item, _status,
onSuccess, onFailure, onCancel
).resizeTo();
);
}
}, 100);
});

View File

@ -10,6 +10,7 @@
import { getNodeListById } from '../../../../static/js/node_ajax';
import ServerSchema from './server.ui';
import Notify from '../../../../../static/js/helpers/Notifier';
import { showServerPassword } from '../../../../static/js/password_dialogs';
define('pgadmin.node.server', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore', 'backbone',
@ -885,10 +886,11 @@ define('pgadmin.node.server', [
if (msg == 'CRYPTKEY_SET') {
connect_to_server(_node, _data, _tree, _item, _wasConnected);
} else if (msg != 'CRYPTKEY_NOT_SET') {
Alertify.dlgServerPass(
showServerPassword(
gettext('Connect to Server'),
msg, _node, _data, _tree, _item, _wasConnected
).resizeTo();
msg, _node, _data, _tree, _item, _wasConnected, onSuccess,
onFailure, onCancel
);
}
}, 100);
});
@ -954,88 +956,6 @@ define('pgadmin.node.server', [
}
};
// Ask Password and send it back to the connect server
if (!Alertify.dlgServerPass) {
Alertify.dialog('dlgServerPass', function factory() {
return {
main: function(
title, message, node, _data, _tree, _item,
_status, _onSuccess, _onFailure, _onCancel
) {
this.set('title', title);
this.message = message;
this.tree = _tree;
this.nodeData = _data;
this.nodeItem = _item;
this.node= node;
this.connected = _status;
this.onSuccess = _onSuccess || onSuccess;
this.onFailure = _onFailure || onFailure;
this.onCancel = _onCancel || onCancel;
},
setup:function() {
return {
buttons:[{
text: gettext('Cancel'), className: 'btn btn-secondary fa fa-times pg-alertify-button',
key: 27,
},{
text: gettext('OK'), key: 13, className: 'btn btn-primary fa fa-check pg-alertify-button',
}],
focus: {element: '#password', select: true},
options: {
modal: 0, resizable: false, maximizable: false, pinnable: false,
},
};
},
build:function() {/*This is intentional (SonarQube)*/},
prepare:function() {
this.setContent(this.message);
},
callback: function(closeEvent) {
var _tree = this.tree,
_item = this.nodeItem,
_node = this.node,
_data = this.nodeData,
_status = this.connected,
_onSuccess = this.onSuccess,
_onFailure = this.onFailure,
_onCancel = this.onCancel;
if (closeEvent.button.text == gettext('OK')) {
var _url = _node.generate_url(_item, 'connect', _data, true);
if (!_status) {
_tree.setLeaf(_item);
_tree.removeIcon(_item);
_tree.addIcon(_item, {icon: 'icon-server-connecting'});
}
$.ajax({
type: 'POST',
timeout: 30000,
url: _url,
data: $('#frmPassword').serialize(),
})
.done(function(res) {
return _onSuccess(
res, _node, _data, _tree, _item, _status
);
})
.fail(function(xhr, status, error) {
return _onFailure(
xhr, status, error, _node, _data, _tree, _item, _status
);
});
} else {
_onCancel && typeof(_onCancel) == 'function' &&
_onCancel(_tree, _item, _data, _status);
}
},
};
});
}
var onCancel = function(_tree, _item, _data, _status) {
_data.is_connecting = false;
_tree.unload(_item);

View File

@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useState, useRef, useEffect } from 'react';
import gettext from 'sources/gettext';
import { Box } from '@material-ui/core';
import { DefaultButton, PrimaryButton } from '../../../static/js/components/Buttons';
@ -8,8 +8,10 @@ import PropTypes from 'prop-types';
import { useModalStyles } from '../../../static/js/helpers/ModalProvider';
import { FormFooterMessage, InputCheckbox, InputText, MESSAGE_TYPE } from '../../../static/js/components/FormComponents';
export default function ConnectServerContent({closeModal, data, onOK}) {
export default function ConnectServerContent({closeModal, data, onOK, setHeight}) {
const classes = useModalStyles();
const containerRef = useRef();
const firstEleRef = useRef();
const [formData, setFormData] = useState({
tunnel_password: '',
save_tunnel_password: false,
@ -25,12 +27,22 @@ export default function ConnectServerContent({closeModal, data, onOK}) {
setFormData((prev)=>({...prev, [id]: val}));
};
useEffect(()=>{
setTimeout(()=>{
firstEleRef.current && firstEleRef.current.focus();
}, 275);
}, []);
useEffect(()=>{
setHeight?.(containerRef.current?.offsetHeight);
}, [containerRef.current]);
if(!data) {
return <>No data</>;
}
return (
<Box display="flex" flexDirection="column" height="100%">
<Box display="flex" flexDirection="column" className={classes.container} ref={containerRef}>
<Box flexGrow="1" p={2}>
{data.prompt_tunnel_password && <>
<Box>
@ -42,10 +54,10 @@ export default function ConnectServerContent({closeModal, data, onOK}) {
</span>
</Box>
<Box marginTop='12px'>
<InputText type="password" value={formData['tunnel_password']} maxLength={null}
onChange={(e)=>onTextChange(e, 'tunnel_password')} autoFocus />
<InputText inputRef={firstEleRef} type="password" value={formData['tunnel_password']} maxLength={null}
onChange={(e)=>onTextChange(e, 'tunnel_password')} />
</Box>
<Box marginTop='12px'>
<Box marginTop='12px' marginBottom='12px'>
<InputCheckbox controlProps={{label: gettext('Save Password')}} value={formData['save_tunnel_password']}
onChange={(e)=>onTextChange(e.target.checked, 'save_tunnel_password')} disabled={!data.allow_save_tunnel_password} />
</Box>
@ -60,8 +72,13 @@ export default function ConnectServerContent({closeModal, data, onOK}) {
</span>
</Box>
<Box marginTop='12px'>
<InputText type="password" value={formData['password']} maxLength={null}
onChange={(e)=>onTextChange(e, 'password')} autoFocus />
<InputText inputRef={(ele)=>{
if(!data.prompt_tunnel_password) {
/* Set only if no tunnel password asked */
firstEleRef.current = ele;
}
}} type="password" value={formData['password']} maxLength={null}
onChange={(e)=>onTextChange(e, 'password')} />
</Box>
<Box marginTop='12px'>
<InputCheckbox controlProps={{label: gettext('Save Password')}} value={formData['save_password']}
@ -99,5 +116,6 @@ export default function ConnectServerContent({closeModal, data, onOK}) {
ConnectServerContent.propTypes = {
closeModal: PropTypes.func,
data: PropTypes.object,
onOK: PropTypes.func
onOK: PropTypes.func,
setHeight: PropTypes.func
};

View File

@ -498,9 +498,10 @@ define('pgadmin.browser.node', [
return null;
},
addUtilityPanel: function(width, height) {
addUtilityPanel: function(width, height, docker) {
var body = window.document.body,
el = document.createElement('div');
el = document.createElement('div'),
dockerObject = docker || pgBrowser.docker;
body.insertBefore(el, body.firstChild);
@ -513,7 +514,7 @@ define('pgadmin.browser.node', [
new_height = height;
}
var new_panel = pgBrowser.docker.addPanel(
var new_panel = dockerObject.addPanel(
'utility_props', window.wcDocker.DOCK.FLOAT, undefined, {
w: new_width,
h: new_height,
@ -548,8 +549,8 @@ define('pgadmin.browser.node', [
}
},
registerUtilityPanel: function() {
var w = pgBrowser.docker,
registerUtilityPanel: function(docker) {
var w = docker || pgBrowser.docker,
p = w.findPanels('utility_props');
if (p && p.length == 1)
@ -572,7 +573,7 @@ define('pgadmin.browser.node', [
},
events: events,
});
p.load(pgBrowser.docker);
p.load(w);
},
register_node_panel: function() {
var w = pgBrowser.docker,

View File

@ -0,0 +1,132 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import React from 'react';
import ReactDOM from 'react-dom';
import pgAdmin from 'sources/pgadmin';
import ConnectServerContent from './ConnectServerContent';
import Theme from 'sources/Theme';
import url_for from 'sources/url_for';
import getApiInstance from '../../../static/js/api_instance';
function setNewSize(panel, width, height) {
// Add height of the header
let newHeight = height + 31;
// Set min and max size of the panel
panel.minSize(width, newHeight);
panel.maxSize(width, newHeight);
panel.maximisable(false);
/* No other way to update size, below is the only way */
panel._parent._size.x = width;
panel._parent._size.y = newHeight;
panel._parent.__update();
}
// This functions is used to show the connect server password dialog.
export function showServerPassword() {
var pgBrowser = pgAdmin.Browser,
title = arguments[0],
formJson = arguments[1],
nodeObj = arguments[2],
nodeData = arguments[3],
treeNodeInfo = arguments[4],
itemNodeData = arguments[5],
status = arguments[6],
onSuccess = arguments[7],
onFailure = arguments[8];
// Register dialog panel
pgBrowser.Node.registerUtilityPanel();
var panel = pgBrowser.Node.addUtilityPanel(pgBrowser.stdW.md),
j = panel.$container.find('.obj_properties').first();
panel.title(title);
ReactDOM.render(
<Theme>
<ConnectServerContent
setHeight={(containerHeight)=>{
setNewSize(panel, pgBrowser.stdW.md, containerHeight);
}}
closeModal={()=>{
panel.close();
}}
data={formJson}
onOK={(formData)=>{
const api = getApiInstance();
var _url = nodeObj.generate_url(itemNodeData, 'connect', nodeData, true);
if (!status) {
treeNodeInfo.setLeaf(itemNodeData);
treeNodeInfo.removeIcon(itemNodeData);
treeNodeInfo.addIcon(itemNodeData, {icon: 'icon-server-connecting'});
}
api.post(_url, formData)
.then(res=>{
panel.close();
return onSuccess(
res.data, nodeObj, nodeData, treeNodeInfo, itemNodeData, status
);
})
.catch((err)=>{
return onFailure(
err.response.request, status, err, nodeObj, nodeData, treeNodeInfo,
itemNodeData, status
);
});
}}
/>
</Theme>, j[0]);
}
// This functions is used to show the connect server password dialog when
// launch from Schema Diff tool.
export function showSchemaDiffServerPassword() {
var pgBrowser = pgAdmin.Browser,
docker = arguments[0],
title = arguments[1],
formJson = arguments[2],
serverID = arguments[3],
successCallback = arguments[4],
onSuccess = arguments[5],
onFailure = arguments[6];
// Register dialog panel
pgBrowser.Node.registerUtilityPanel(docker);
var panel = pgBrowser.Node.addUtilityPanel(pgBrowser.stdW.md, undefined, docker),
j = panel.$container.find('.obj_properties').first();
panel.title(title);
ReactDOM.render(
<Theme>
<ConnectServerContent
setHeight={(containerHeight)=>{
setNewSize(panel, pgBrowser.stdW.md, containerHeight);
}}
closeModal={()=>{
panel.close();
}}
data={formJson}
onOK={(formData)=>{
const api = getApiInstance();
var _url = url_for('schema_diff.connect_server', {'sid': serverID});
api.post(_url, formData)
.then(res=>{
panel.close();
return onSuccess(res.data, successCallback);
})
.catch((err)=>{
return onFailure(
err.response.request, status, err, serverID, successCallback
);
});
}}
/>
</Theme>, j[0]);
}

View File

@ -92,5 +92,5 @@ class BrowserToolBarFeatureTest(BaseFeatureTest):
BrowserToolBarLocators.filter_data_button_css),
(By.CSS_SELECTOR, BrowserToolBarLocators.filter_alertify_box_css)),
'Filter dialogue did not open on clicking filter button.')
self.page.click_modal('Cancel')
self.page.click_modal('Close', True)
self.page.close_query_tool(prompt=False)

View File

@ -210,6 +210,10 @@ PaperComponent.propTypes = {
};
export const useModalStyles = makeStyles((theme) => ({
container: {
backgroundColor: theme.palette.background.default
},
titleBar: {
display: 'flex',
flexGrow: 1

View File

@ -19,6 +19,7 @@ import 'pgadmin.tools.sqleditor';
import pgWindow from 'sources/window';
import _ from 'underscore';
import Notify from '../../../../static/js/helpers/Notifier';
import { showSchemaDiffServerPassword } from '../../../../browser/static/js/password_dialogs';
import { SchemaDiffSelect2Control, SchemaDiffHeaderView,
@ -920,17 +921,21 @@ export default class SchemaDiffUI {
}
connect_server(server_id, callback) {
let self = this;
var onFailure = function(
xhr, status, error, sid, err_callback
) {
Notify.pgNotifier('error', xhr, error, function(msg) {
setTimeout(function() {
Alertify.dlgServerPass(
showSchemaDiffServerPassword(
self.docker,
gettext('Connect to Server'),
msg,
sid,
err_callback
).resizeTo();
err_callback,
onSuccess,
onFailure
);
}, 100);
});
},
@ -941,79 +946,6 @@ export default class SchemaDiffUI {
}
};
// Ask Password and send it back to the connect server
if (!Alertify.dlgServerPass) {
Alertify.dialog('dlgServerPass', function factory() {
return {
main: function(
title, message, sid, success_callback, _onSuccess, _onFailure, _onCancel
) {
this.set('title', title);
this.message = message;
this.server_id = sid;
this.success_callback = success_callback;
this.onSuccess = _onSuccess || onSuccess;
this.onFailure = _onFailure || onFailure;
this.onCancel = _onCancel || onCancel;
},
setup:function() {
return {
buttons:[{
text: gettext('Cancel'), className: 'btn btn-secondary fa fa-times pg-alertify-button',
key: 27,
},{
text: gettext('OK'), key: 13, className: 'btn btn-primary fa fa-check pg-alertify-button',
}],
focus: {element: '#password', select: true},
options: {
modal: 0, resizable: false, maximizable: false, pinnable: false,
},
};
},
build:function() {/*This is intentional (SonarQube)*/},
prepare:function() {
this.setContent(this.message);
},
callback: function(closeEvent) {
var _onFailure = this.onFailure,
_onSuccess = this.onSuccess,
_onCancel = this.onCancel,
_success_callback = this.success_callback;
if (closeEvent.button.text == gettext('OK')) {
var _url = url_for('schema_diff.connect_server', {'sid': this.server_id});
$.ajax({
type: 'POST',
timeout: 30000,
url: _url,
data: $('#frmPassword').serialize(),
})
.done(function(res) {
if (res.success == 1) {
return _onSuccess(res, _success_callback);
}
})
.fail(function(xhr, status, error) {
return _onFailure(
xhr, status, error, this.server_id, _success_callback
);
});
} else {
_onCancel && typeof(_onCancel) == 'function' &&
_onCancel();
}
},
};
});
}
var onCancel = function() {
return false;
};
var url = url_for('schema_diff.connect_server', {'sid': server_id});
$.post(url)
.done(function(res) {

View File

@ -134,7 +134,6 @@ class SqlEditorModule(PgAdminModule):
'sqleditor._check_server_connection_status',
'sqleditor.get_new_connection_role',
'sqleditor.connect_server',
'sqleditor.connect_server_with_user',
]
def on_logout(self, user):
@ -2294,39 +2293,29 @@ def get_new_connection_role(sgid, sid=None):
)
@blueprint.route(
'/connect_server/<int:sid>/<usr>',
methods=["POST"],
endpoint="connect_server_with_user"
)
@blueprint.route(
'/connect_server/<int:sid>',
methods=["POST"],
endpoint="connect_server"
)
@login_required
def connect_server(sid, usr=None):
def connect_server(sid):
# Check if server is already connected then no need to reconnect again.
server = Server.query.filter_by(id=sid).first()
driver = get_driver(PG_DEFAULT_DRIVER)
manager = driver.connection_manager(sid)
conn = manager.connection()
user = None
if usr and manager.user != usr:
user = usr
else:
user = manager.user
if conn.connected():
return make_json_response(
success=1,
info=gettext("Server connected."),
data={}
)
conn = manager.connection()
if conn.connected():
return make_json_response(
success=1,
info=gettext("Server connected."),
data={}
)
view = SchemaDiffRegistry.get_node_view('server')
return view.connect(
server.servergroup_id, sid, user_name=user, resp_json=True
server.servergroup_id, sid
)

View File

@ -24,7 +24,7 @@ class BrowserToolBarLocators():
filter_data_button_css = \
".wcFrameButton[title='Filtered Rows']:not(.disabled)"
filter_alertify_box_css = ".alertify .ajs-header[data-title~='Filter']"
filter_alertify_box_css = ".wcPanelTab.wcPanelTabActive.wcNotMoveable"
class NavMenuLocators: