1) Port change password dialog to React. Fixes #7341

2) Port named restore point dialog to React. Fixes #7546
This commit is contained in:
Akshay Joshi 2022-07-11 13:39:09 +05:30
parent 1a5e60c44f
commit 02b0f35442
12 changed files with 319 additions and 207 deletions

View File

@ -19,7 +19,7 @@ the restore point to add* to provide a descriptive name for the restore point.
For more information about using a restore point as a recovery target, please
see the
`PostgreSQL documentation <https://www.postgresql.org/docs/current/recovery-target-settings.html#RECOVERY-TARGET-NAME>`_.
`PostgreSQL documentation <https://www.postgresql.org/docs/current/runtime-config-wal.html#RUNTIME-CONFIG-WAL-RECOVERY-TARGET>`_.
* Click the *OK* button to save the restore point.
* Click the *Cancel* button to exit without saving work.
* Click the *Cancel* button to exit without saving work.

View File

@ -36,5 +36,8 @@ Use the *Change Password* dialog to change your password:
* Enter the desired password for in the *New Password* field.
* Re-enter the new password in the *Confirm Password* field.
Click the *OK* button to change your password; click *Cancel* to exit the dialog
without changing your password.
Click the *Save* button to change your password.
Click the *Close* button to exit without changing your password.
Click the *Reset* button to reset the values.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 37 KiB

View File

@ -15,8 +15,10 @@ New features
Housekeeping
************
| `Issue #7341 <https://redmine.postgresql.org/issues/7341>`_ - Port change password dialog to React.
| `Issue #7342 <https://redmine.postgresql.org/issues/7342>`_ - Port Master Password dialog to React.
| `Issue #7492 <https://redmine.postgresql.org/issues/7492>`_ - Removing dynamic module loading and replacing it with static loading.
| `Issue #7546 <https://redmine.postgresql.org/issues/7546>`_ - Port named restore point dialog to React.
Bug fixes
*********

View File

@ -1615,9 +1615,10 @@ class ServerNode(PGChildNodeView):
sid: Server id
"""
try:
if request.form and request.form['data']:
data = json.loads(request.form['data'], encoding='utf-8')
else:
data = None
if request.form:
data = request.form
elif request.data:
data = json.loads(request.data, encoding='utf-8')
crypt_key = get_crypt_key()[1]

View File

@ -10,18 +10,17 @@
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';
import { showServerPassword, showChangeServerPassword, showNamedRestorePoint } from '../../../../static/js/password_dialogs';
define('pgadmin.node.server', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore', 'backbone',
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'sources/pgadmin', 'pgadmin.browser',
'pgadmin.user_management.current_user',
'pgadmin.alertifyjs', 'pgadmin.backform',
'pgadmin.authenticate.kerberos',
'pgadmin.browser.server.privilege',
], function(
gettext, url_for, $, _, Backbone, pgAdmin, pgBrowser,
current_user, Alertify, Backform, Kerberos,
gettext, url_for, $, _, pgAdmin, pgBrowser,
current_user, Kerberos,
) {
if (!pgBrowser.Nodes['server']) {
@ -377,34 +376,7 @@ define('pgadmin.node.server', [
if (!d)
return false;
Alertify.prompt(
gettext('Enter the name of the restore point to add'), '',
// We will execute this function when user clicks on the OK button
function(evt, value) {
// If user has provided a value, send it to the server
if(!_.isUndefined(value) && !_.isNull(value) && value !== ''
&& String(value).replace(/^\s+|\s+$/g, '') !== '') {
$.ajax({
url: obj.generate_url(i, 'restore_point', d, true),
method:'POST',
data:{ 'value': JSON.stringify(value) },
})
.done(function(res) {
Notify.success(res.data.result, 10000);
})
.fail(function(xhr, status, error) {
Notify.pgRespErrorNotify(xhr, error);
t.unload(i);
});
} else {
evt.cancel = true;
Notify.error(gettext('Please enter a valid name.'), 10000);
}
},
// We will execute this function when user clicks on the Cancel
// button. Do nothing just close it.
function(evt) { evt.cancel = false; }
).set({'title': gettext('Restore point name')});
showNamedRestorePoint(gettext('Restore point name'), d, obj, i);
},
/* Change password */
@ -414,162 +386,10 @@ define('pgadmin.node.server', [
t = pgBrowser.tree,
i = input.item || t.selected(),
d = i ? t.itemData(i) : undefined,
url = obj.generate_url(i, 'change_password', d, true),
is_pgpass_file_used = false,
check_pgpass_url = obj.generate_url(i, 'check_pgpass', d, true);
if (d) {
if(!Alertify.changeServerPassword) {
var newPasswordModel = Backbone.Model.extend({
defaults: {
user_name: undefined,
password: undefined,
newPassword: undefined,
confirmPassword: undefined,
},
validate: function() {
return null;
},
}),
passwordChangeFields = [{
name: 'user_name', label: gettext('User'),
type: 'text', readonly: true, control: 'input',
},{
name: 'password', label: gettext('Current Password'),
type: 'password', disabled: function() { return is_pgpass_file_used; },
control: 'input', required: true,
},{
name: 'newPassword', label: gettext('New Password'),
type: 'password', disabled: false, control: 'input',
required: true,
},{
name: 'confirmPassword', label: gettext('Confirm Password'),
type: 'password', disabled: false, control: 'input',
required: true,
}];
Alertify.dialog('changeServerPassword' ,function factory() {
return {
main: function(params) {
var title = gettext('Change Password');
this.set('title', title);
this.user_name = params.user.name;
},
setup:function() {
return {
buttons: [{
text: gettext('Cancel'), key: 27,
className: 'btn btn-secondary fa fa-times pg-alertify-button', attrs: {name: 'cancel'},
},{
text: gettext('OK'), key: 13, className: 'btn btn-primary fa fa-check pg-alertify-button',
attrs: {name:'submit'},
}],
// Set options for dialog
options: {
padding : !1,
overflow: !1,
modal:false,
resizable: true,
maximizable: true,
pinnable: false,
closableByDimmer: false,
},
};
},
hooks: {
// triggered when the dialog is closed
onclose: function() {
if (this.view) {
this.view.remove({data: true, internal: true, silent: true});
}
},
},
prepare: function() {
var self = this;
// Disable Ok button until user provides input
this.__internal.buttons[1].element.disabled = true;
var $container = $('<div class=\'change_password\'></div>'),
newpasswordmodel = new newPasswordModel(
{'user_name': self.user_name}
),
view = this.view = new Backform.Form({
el: $container,
model: newpasswordmodel,
fields: passwordChangeFields,
});
view.render();
this.elements.content.appendChild($container.get(0));
// Listen to model & if filename is provided then enable Backup button
this.view.model.on('change', function() {
var that = this,
password = this.get('password'),
newPassword = this.get('newPassword'),
confirmPassword = this.get('confirmPassword');
// Only check password field if pgpass file is not available
if ((!is_pgpass_file_used &&
(_.isUndefined(password) || _.isNull(password) || password == '')) ||
_.isUndefined(newPassword) || _.isNull(newPassword) || newPassword == '' ||
_.isUndefined(confirmPassword) || _.isNull(confirmPassword) || confirmPassword == '') {
self.__internal.buttons[1].element.disabled = true;
} else if (newPassword != confirmPassword) {
self.__internal.buttons[1].element.disabled = true;
this.errorTimeout && clearTimeout(this.errorTimeout);
this.errorTimeout = setTimeout(function() {
that.errorModel.set('confirmPassword', gettext('Passwords do not match.'));
} ,400);
}else {
that.errorModel.clear();
self.__internal.buttons[1].element.disabled = false;
}
});
},
// Callback functions when click on the buttons of the Alertify dialogs
callback: function(e) {
if (e.button.element.name == 'submit') {
var self = this,
alertArgs = this.view.model.toJSON();
e.cancel = true;
$.ajax({
url: url,
method:'POST',
data:{'data': JSON.stringify(alertArgs) },
})
.done(function(res) {
if (res.success) {
// Notify user to update pgpass file
if(is_pgpass_file_used) {
Notify.alert(
gettext('Change Password'),
gettext('Please make sure to disconnect the server'
+ ' and update the new password in the pgpass file'
+ ' before performing any other operation')
);
}
Notify.success(res.info);
self.close();
} else {
Notify.error(res.errormsg);
}
})
.fail(function(xhr, status, error) {
Notify.pgRespErrorNotify(xhr, error);
});
}
},
};
});
}
// Call to check if server is using pgpass file or not
$.ajax({
url: check_pgpass_url,
@ -579,7 +399,7 @@ define('pgadmin.node.server', [
if (res.success && res.data.is_pgpass) {
is_pgpass_file_used = true;
}
Alertify.changeServerPassword(d).resizeTo('40%','52%');
showChangeServerPassword(gettext('Change Password'), d, obj, i, is_pgpass_file_used);
})
.fail(function(xhr, status, error) {
Notify.pgRespErrorNotify(xhr, error);

View File

@ -0,0 +1,99 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import { makeStyles } from '@material-ui/core';
import React from 'react';
import PropTypes from 'prop-types';
import gettext from 'sources/gettext';
import BaseUISchema from '../../../static/js/SchemaView/base_schema.ui';
import SchemaView from '../../../static/js/SchemaView';
class ChangePasswordSchema extends BaseUISchema {
constructor(user, isPgpassFileUsed) {
super({
user: user,
password: '',
newPassword: '',
confirmPassword: ''
});
this.isPgpassFileUsed = isPgpassFileUsed;
}
get baseFields() {
let self = this;
return [
{
id: 'user', label: gettext('User'), type: 'text', disabled: true
}, {
id: 'password', label: gettext('Current Password'), type: 'password',
disabled: self.isPgpassFileUsed, noEmpty: self.isPgpassFileUsed ? false : true,
controlProps: {
maxLength: null
}
}, {
id: 'newPassword', label: gettext('New Password'), type: 'password',
noEmpty: true,
controlProps: {
maxLength: null
}
}, {
id: 'confirmPassword', label: gettext('Confirm Password'), type: 'password',
noEmpty: true,
controlProps: {
maxLength: null
}
}
];
}
validate(state, setError) {
let errmsg = null;
if (state.newPassword !== state.confirmPassword) {
errmsg = gettext('Passwords do not match.');
setError('confirmPassword', errmsg);
return true;
} else {
setError('confirmPassword', null);
}
return false;
}
}
const useStyles = makeStyles((theme)=>({
root: {
...theme.mixins.tabPanel,
},
}));
export default function ChangePasswordContent({onSave, onClose, userName, isPgpassFileUsed}) {
const classes = useStyles();
return<SchemaView
formType={'dialog'}
getInitData={() => { /*This is intentional (SonarQube)*/ }}
schema={new ChangePasswordSchema(userName, isPgpassFileUsed)}
viewHelperProps={{
mode: 'create',
}}
onSave={onSave}
onClose={onClose}
hasSQL={false}
disableSqlHelp={true}
disableDialogHelp={true}
isTabView={false}
formClassName={classes.root}
/>;
}
ChangePasswordContent.propTypes = {
onSave: PropTypes.func,
onClose: PropTypes.func,
userName: PropTypes.string,
isPgpassFileUsed: PropTypes.bool
};

View File

@ -1,3 +1,12 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import React, { useState, useRef, useEffect } from 'react';
import gettext from 'sources/gettext';
import { Box } from '@material-ui/core';

View File

@ -1,3 +1,12 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import React, { useState, useRef, useEffect } from 'react';
import PropTypes from 'prop-types';
import gettext from 'sources/gettext';

View File

@ -0,0 +1,90 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
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';
import CloseIcon from '@material-ui/icons/CloseRounded';
import CheckRoundedIcon from '@material-ui/icons/CheckRounded';
import PropTypes from 'prop-types';
import { useModalStyles } from '../../../static/js/helpers/ModalProvider';
import { InputText } from '../../../static/js/components/FormComponents';
import { isEmptyString } from '../../../static/js/validators';
export default function NamedRestoreContent({closeModal, onOK, setHeight}) {
const classes = useModalStyles();
const containerRef = useRef();
const firstEleRef = useRef();
const okBtnRef = useRef();
const [formData, setFormData] = useState({
namedRestorePoint: ''
});
const onTextChange = (e, id) => {
let val = e;
if(e && e.target) {
val = e.target.value;
}
setFormData((prev)=>({...prev, [id]: val}));
};
const onKeyDown = (e) => {
// If enter key is pressed then click on OK button
if (e.key === 'Enter') {
okBtnRef.current?.click();
}
};
useEffect(()=>{
setTimeout(()=>{
firstEleRef.current && firstEleRef.current.focus();
}, 275);
}, []);
useEffect(()=>{
setHeight?.(containerRef.current?.offsetHeight);
}, [containerRef.current]);
const isOKDisabled = isEmptyString(formData.namedRestorePoint);
return (
<Box display="flex" flexDirection="column" className={classes.container} ref={containerRef}>
<Box flexGrow="1" p={2}>
<Box>
<span style={{fontWeight: 'bold'}}>
{gettext('Enter the name of the restore point to add')}
</span>
</Box>
<Box marginTop='12px'>
<InputText inputRef={firstEleRef} type="text" value={formData['namedRestorePoint']}
onChange={(e)=>onTextChange(e, 'namedRestorePoint')} onKeyDown={(e)=>onKeyDown(e)}/>
</Box>
</Box>
<Box className={classes.footer}>
<DefaultButton data-test="close" startIcon={<CloseIcon />} onClick={()=>{
closeModal();
}} >{gettext('Cancel')}</DefaultButton>
<PrimaryButton ref={okBtnRef} data-test="save" disabled={isOKDisabled} className={classes.margin} startIcon={<CheckRoundedIcon />} onClick={()=>{
let postFormData = new FormData();
postFormData.append('value', formData.namedRestorePoint);
onOK?.(postFormData);
closeModal();
}} >{gettext('OK')}</PrimaryButton>
</Box>
</Box>
);
}
NamedRestoreContent.propTypes = {
closeModal: PropTypes.func,
data: PropTypes.object,
onOK: PropTypes.func,
setHeight: PropTypes.func
};

View File

@ -17,6 +17,8 @@ import gettext from 'sources/gettext';
import getApiInstance from '../../../static/js/api_instance';
import MasterPasswordContent from './MasterPassowrdContent';
import ChangePasswordContent from './ChangePassowrdContent';
import NamedRestoreContent from './NamedRestoreContent';
import Notify from '../../../static/js/helpers/Notifier';
function setNewSize(panel, width, height) {
@ -154,6 +156,7 @@ export function checkMasterPassword(data, masterpass_callback_queue, cancel_call
Notify.pgRespErrorNotify(xhr, error);
});
}
// This functions is used to show the master password dialog.
export function showMasterPassword(isPWDPresent, errmsg=null, masterpass_callback_queue, cancel_callback) {
const api = getApiInstance();
@ -204,22 +207,98 @@ export function showMasterPassword(isPWDPresent, errmsg=null, masterpass_callbac
onOK={(formData) => {
panel.close();
checkMasterPassword(formData, masterpass_callback_queue, cancel_callback);
// var _url = url_for('browser.set_master_password');
// api.post(_url, formData)
// .then(res => {
// panel.close();
// if(res.data.data.is_error) {
// showMasterPassword(true, res.data.data.errmsg, masterpass_callback_queue, cancel_callback);
// } else {
// masterPassCallbacks(masterpass_callback_queue);
// }
// })
// .catch((err) => {
// Notify.error(err.message);
// });
}}
/>
</Theme>, j[0]);
}
export function showChangeServerPassword() {
var pgBrowser = pgAdmin.Browser,
title = arguments[0],
nodeData = arguments[1],
nodeObj = arguments[2],
itemNodeData = arguments[3],
isPgPassFileUsed = arguments[4];
// 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>
<ChangePasswordContent
onClose={()=>{
panel.close();
}}
onSave={(isNew, data)=>{
return new Promise((resolve, reject)=>{
const api = getApiInstance();
var _url = nodeObj.generate_url(itemNodeData, 'change_password', nodeData, true);
api.post(_url, data)
.then(({data: respData})=>{
Notify.success(respData.info);
// Notify user to update pgpass file
if(isPgPassFileUsed) {
Notify.alert(
gettext('Change Password'),
gettext('Please make sure to disconnect the server'
+ ' and update the new password in the pgpass file'
+ ' before performing any other operation')
);
}
resolve(respData.data);
panel.close();
})
.catch((error)=>{
reject(error);
});
});
}}
userName={nodeData.user.name}
isPgpassFileUsed={isPgPassFileUsed}
/>
</Theme>, j[0]);
}
export function showNamedRestorePoint() {
var pgBrowser = pgAdmin.Browser,
title = arguments[0],
nodeData = arguments[1],
nodeObj = arguments[2],
itemNodeData = arguments[3];
// 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>
<NamedRestoreContent
setHeight={(containerHeight)=>{
setNewSize(panel, pgBrowser.stdW.md, containerHeight);
}}
closeModal={()=>{
panel.close();
}}
onOK={(formData)=>{
const api = getApiInstance();
var _url = nodeObj.generate_url(itemNodeData, 'restore_point', nodeData, true);
api.post(_url, formData)
.then(res=>{
panel.close();
Notify.success(res.data.data.result);
})
.catch(function(xhr, status, error) {
Notify.pgRespErrorNotify(xhr, error);
});
}}
/>
</Theme>, j[0]);
}