Ensure proper error message shown if any error occurs while restoring psql tool.

pull/8902/head
Yogesh Mahajan 2025-06-27 13:35:24 +05:30 committed by GitHub
parent 7c2b773ad1
commit 51d3fe54f3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 186 additions and 128 deletions

View File

@ -329,7 +329,7 @@ Use the fields on the *User Interface* panel to set the user interface related p
format. If the application is closed unexpectedly, the tab is accidentally closed,
or the page is refreshed, the saved state will be automatically restored for
each respective tool. **Note:** Saving the application state will not preserve data for tool tabs opened in
separate browser tabs when running in server mode.
separate browser tabs when running in server mode.Any tool referring ad-hoc server connection will not be restored.
* Use the *Themes* drop-down listbox to select the theme for pgAdmin. You'll also get a preview just below the
drop down. You can also submit your own themes,

View File

@ -5,6 +5,7 @@ import { LAYOUT_EVENTS } from './helpers/Layout';
import { styled } from '@mui/material/styles';
import { FormHelperText, Box } from '@mui/material';
import HTMLReactParse from 'html-react-parser';
import { deleteToolData } from '../../settings/static/ApplicationStateProvider';
const StyledBox = styled(Box)(({theme}) => ({
color: theme.palette.text.primary,
@ -15,15 +16,18 @@ const StyledBox = styled(Box)(({theme}) => ({
height: '100%',
}));
export default function ToolErrorView({error, panelId, panelDocker}){
export default function ToolErrorView({error, panelId, panelDocker, toolDataId}){
panelDocker.eventBus.registerListener(LAYOUT_EVENTS.CLOSING, (id)=>{
if(panelId == id) {
panelDocker.close(panelId, true);
if(toolDataId){
let transId = toolDataId.replace('-','_');
deleteToolData(transId, transId);
}
}
});
let err_msg = gettext(`There was some error while opening: ${error}`);
let err_msg = gettext(`An error occurred while opening the tool: ${error}`);
return (<StyledBox>
<FormHelperText variant="outlined" error= {true} style={{marginLeft: '4px'}} >{HTMLReactParse(err_msg)}</FormHelperText>
</StyledBox>);
@ -33,4 +37,5 @@ ToolErrorView.propTypes = {
error: PropTypes.string,
panelId: PropTypes.string,
panelDocker: PropTypes.object,
toolDataId: PropTypes.string
};

View File

@ -10,7 +10,7 @@
"""A blueprint module implementing the erd tool."""
import json
from flask import request, Response
from flask import request, Response, session
from flask import render_template, current_app as app
from flask_security import permissions_required
from pgadmin.user_login_check import pga_login_required
@ -20,7 +20,7 @@ from pgadmin.utils import PgAdminModule, \
SHORTCUT_FIELDS as shortcut_fields
from pgadmin.utils.ajax import make_json_response, internal_server_error
from pgadmin.model import Server
from config import PG_DEFAULT_DRIVER
from config import PG_DEFAULT_DRIVER, ALLOW_SAVE_PASSWORD
from pgadmin.utils.driver import get_driver
from pgadmin.browser.utils import underscore_unescape
from pgadmin.browser.server_groups.servers.databases.schemas.utils \
@ -540,11 +540,26 @@ def initialize_erd(trans_id, sgid, sid, did):
"""
# Read the data if present. Skipping read may cause connection
# reset error if data is sent from the client
data = {}
if request.data:
_ = request.data
conn = _get_connection(sid, did, trans_id)
data = json.loads(request.data)
try:
conn = _get_connection(sid, did, trans_id, data.get('db_name', None))
except ConnectionLost as e:
return make_json_response(
success=0,
status=428,
result={"server_label": data.get('server_name', None),
"username": data.get('user', None),
"server_type":data.get('server_type', None),
"errmsg": str(e),
"prompt_password": True,
"allow_save_password": True
if ALLOW_SAVE_PASSWORD and
session.get('allow_save_password', None) else False,
}
)
return make_json_response(
data={
'connId': str(trans_id),
@ -554,7 +569,7 @@ def initialize_erd(trans_id, sgid, sid, did):
)
def _get_connection(sid, did, trans_id):
def _get_connection(sid, did, trans_id, db_name=None):
"""
Get the connection object of ERD.
:param sid:
@ -564,9 +579,13 @@ def _get_connection(sid, did, trans_id):
"""
manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(sid)
try:
conn = manager.connection(did=did, conn_id=trans_id,
conn = manager.connection(conn_id=trans_id,
auto_reconnect=True,
use_binary_placeholder=True)
use_binary_placeholder=True,
**({"database": db_name}
if db_name is not None
else {"did": did})
)
status, msg = conn.connect()
if not status:
app.logger.error(msg)

View File

@ -96,14 +96,19 @@ export default class ERDModule {
parentData = {
server_group: {
_id: connectionInfo.sgid || 0,
server_type: connectionInfo.server_type
_id: connectionInfo.sgid || 0
},
server: {
_id: connectionInfo.sid,
server_type: connectionInfo.server_type,
label: connectionInfo.server_name,
user: {
name: connectionInfo.user
}
},
database: {
_id: connectionInfo.did,
label: connectionInfo.db_name
},
schema: {
_id: connectionInfo.scid || null,
@ -142,7 +147,7 @@ export default class ERDModule {
'pgadmin:tool:show',
`${BROWSER_PANELS.ERD_TOOL}_${transId}`,
panelUrl,
{sql_id: toolDataId, title: _.escape(panelTitle)},
{sql_id: toolDataId, title: _.escape(panelTitle), db_name:parentData.database.label, server_name: parentData.server.label, user: parentData.server.user.name, server_type: parentData.server.server_type},
{title: 'Untitled', icon: 'fa fa-sitemap'},
Boolean(open_new_tab?.includes('erd_tool'))
);

View File

@ -42,7 +42,7 @@ export const STATUS = {
function ConnectionStatusIcon({status}) {
if(status == STATUS.CONNECTING) {
return <CircularProgress style={{height: '18px', width: '18px'}} />;
} else if(status == STATUS.CONNECTED || status == STATUS.FAILED) {
} else if(status == STATUS.CONNECTED) {
return <ConnectedIcon />;
} else {
return <DisconnectedIcon />;

View File

@ -39,6 +39,7 @@ import BeforeUnload from './BeforeUnload';
import { isMac } from '../../../../../../static/js/keyboard_shortcuts';
import DownloadUtils from '../../../../../../static/js/DownloadUtils';
import { getToolData } from '../../../../../../settings/static/ApplicationStateProvider';
import { connectServerModal, connectServer } from '../../../../../sqleditor/static/js/components/connectServer';
/* Custom react-diagram action for keyboard events */
export class KeyboardShortcutAction extends Action {
@ -348,10 +349,12 @@ export default class ERDTool extends React.Component {
});
let done = await this.initConnection();
if(!done) return;
if(!done && !this.props.params.sql_id) return;
done = await this.loadPrequisiteData();
if(!done) return;
if(done){
done = await this.loadPrequisiteData();
if(!done && !this.props.params.sql_id) return;
}
if(this.props.params.sql_id){
@ -361,7 +364,7 @@ export default class ERDTool extends React.Component {
this.diagram.clearSelection();
this.registerModelEvents();
this.setState({dirty: true});
this.eventBus.fireEvent(ERD_EVENTS.DIRTY, true, this.serializeFile());
this.eventBus.fireEvent(ERD_EVENTS.DIRTY, true, sqlValue);
}
}
else if(this.props.params.gen) {
@ -597,6 +600,7 @@ export default class ERDTool extends React.Component {
current_file: fileName,
dirty: false,
});
this.eventBus.fireEvent(ERD_EVENTS.DIRTY, true, res.data);
this.eventBus.fireEvent(ERD_EVENTS.DIRTY, false);
this.setTitle(fileName);
this.diagram.deserialize(res.data);
@ -857,7 +861,12 @@ export default class ERDTool extends React.Component {
});
try {
let response = await this.apiObj.post(initUrl);
let response = await this.apiObj.post(
initUrl,
{server_name: this.props.params.server_name,
server_type : this.props.params.server_type,
user: this.props.params.user,
db_name: this.props.params.db_name});
this.setState({
conn_status: CONNECT_STATUS.CONNECTED,
server_version: response.data.data.serverVersion,
@ -866,7 +875,16 @@ export default class ERDTool extends React.Component {
return true;
} catch (error) {
this.setState({conn_status: CONNECT_STATUS.FAILED});
this.handleAxiosCatch(error);
connectServerModal(this.context, error.response?.data?.result, async (passwordData)=>{
await connectServer(this.apiObj, this.context, this.props.params.sid, this.props.params.sid, passwordData, async ()=>{
await this.initConnection();
await this.loadPrequisiteData();
});
}, ()=>{
this.setState({conn_status: CONNECT_STATUS.FAILED});
});
return false;
} finally {
this.setLoading(null);
@ -925,6 +943,7 @@ export default class ERDTool extends React.Component {
}
setTimeout(()=>{
this.onAutoDistribute();
this.eventBus.fireEvent(ERD_EVENTS.DIRTY, true, this.serializeFile());
}, 250);
}
@ -975,6 +994,9 @@ ERDTool.propTypes = {
fgcolor: PropTypes.string,
gen: PropTypes.bool.isRequired,
sql_id: PropTypes.string,
server_name: PropTypes.string,
user: PropTypes.string,
db_name: PropTypes.string,
}),
pgWindow: PropTypes.object.isRequired,
pgAdmin: PropTypes.object.isRequired,

View File

@ -101,14 +101,15 @@ def panel(trans_id):
s = Server.query.filter_by(id=int(params['sid'])).first()
if s:
data = _get_database_role(params['sid'], params['did'])
params['db'] = underscore_escape(data['db_name']) \
if 'db_name' in data else 'postgres'
params['role'] = underscore_escape(data['role'])
if data:
params['db'] = underscore_escape(data['db_name']) \
if 'db_name' in data else 'postgres'
params['role'] = underscore_escape(data['role'])
set_env_variables(is_win=_platform == 'win32')
return render_template("psql/index.html",
params=json.dumps(params))
else:
params['error'] = 'Server did not find.'
params['error'] = 'The server was not found.'
return render_template(
"psql/index.html",
params=json.dumps(params))
@ -303,6 +304,7 @@ def start_process(data):
# Check user is authenticated and PSQL is enabled in config.
if current_user.is_authenticated and config.ENABLE_PSQL:
connection_data = []
connection_successful = False
try:
db = ''
if data['db']:
@ -326,33 +328,39 @@ def start_process(data):
return
connection_data = get_connection_str(psql_utility, db,
manager)
connection_successful = True
except Exception as e:
# If any error raised during the start the PSQL emit error to UI.
# request.sid: This sid is socket id.
sio.emit(
'conn_error',
{
'error': 'Error while running psql command: {0}'.format(e),
}, namespace='/pty', room=request.sid)
error_msg = 'Error while running psql command: {0}'.format(e)
if str(e) == 'Server is not connected.':
error_msg = 'Error while opening psql tool: {0}'.format(e)
try:
if str(data['sid']) not in app.config['sid_soid_mapping']:
# request.sid: refer request.sid as socket id.
app.config['sid_soid_mapping'][str(data['sid'])] = list()
app.config['sid_soid_mapping'][str(data['sid'])].append(
request.sid)
else:
app.config['sid_soid_mapping'][str(data['sid'])].append(
request.sid)
sio.emit('conn_error',
{'error': error_msg},
namespace='/pty',
room=request.sid)
sio.start_background_task(read_and_forward_pty_output,
request.sid, data)
except Exception as e:
sio.emit(
'conn_error',
{
'error': 'Error while running psql command: {0}'.format(e),
}, namespace='/pty', room=request.sid)
if connection_successful:
try:
if str(data['sid']) not in app.config['sid_soid_mapping']:
# request.sid: refer request.sid as socket id.
app.config['sid_soid_mapping'][str(data['sid'])] = list()
app.config['sid_soid_mapping'][str(data['sid'])].append(
request.sid)
else:
app.config['sid_soid_mapping'][str(data['sid'])].append(
request.sid)
sio.start_background_task(read_and_forward_pty_output,
request.sid, data)
except Exception as e:
sio.emit(
'conn_error',
{'error':'Error while running psql command: {0}'.
format(e)},
namespace='/pty',
room=request.sid)
else:
# Show error if user is not authenticated.
sio.emit('conn_not_allow', {'sid': request.sid}, namespace='/pty',
@ -368,6 +376,10 @@ def _get_connection(sid, data):
:return:
"""
manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(sid)
if not manager:
msg = 'Server is not connected.'
app.logger.error(msg)
raise RuntimeError(msg)
try:
conn = manager.connection()
# This is added for unit test only, no use in normal execution.
@ -378,12 +390,6 @@ def _get_connection(sid, data):
status, msg = conn.connect()
if not status:
app.logger.error(msg)
sio.emit(sio.emit(
'conn_error',
{
'error': 'Error while running psql command: {0}'
''.format('Server connection not present.'),
}, namespace='/pty', room=request.sid))
raise RuntimeError('Server is not connected.')
return conn, manager

View File

@ -122,7 +122,7 @@ export default class Psql {
},
database: {
_id: connectionInfo.did,
label: connectionInfo.db
_label: connectionInfo.db
},
schema: {
_id: connectionInfo.scid || null,
@ -161,7 +161,7 @@ export default class Psql {
const transId = getRandomInt(1, 9999999);
// Set psql tab title as per prefrences setting.
let title_data = {
'database': parentData.database ? _.unescape(parentData.database.label) : 'postgres' ,
'database': parentData.database ? _.unescape(parentData.database._label) : 'postgres' ,
'username': parentData.server.user.name,
'server': parentData.server.label,
'type': 'psql_tool',

View File

@ -361,7 +361,7 @@ def panel(trans_id):
params=json.dumps(params),
)
else:
params['error'] = 'Server did not find.'
params['error'] = 'The server was not found.'
return render_template(
"sqleditor/index.html",
title=None,

View File

@ -254,6 +254,7 @@ export default class SQLEditor {
error={params.error}
panelId={`${BROWSER_PANELS.QUERY_TOOL}_${params.trans_id}`}
panelDocker={panelDocker}
toolDataId={params.toolDataId}
/> :
<QueryToolComponent params={params}
pgWindow={pgWindow}

View File

@ -35,9 +35,9 @@ import * as Kerberos from 'pgadmin.authenticate.kerberos';
import PropTypes from 'prop-types';
import { retrieveNodeName } from '../show_view_data';
import { useModal } from '../../../../../static/js/helpers/ModalProvider';
import ConnectServerContent from '../../../../../static/js/Dialogs/ConnectServerContent';
import usePreferences from '../../../../../preferences/static/js/store';
import { useApplicationState } from '../../../../../settings/static/ApplicationStateProvider';
import { connectServer, connectServerModal } from './connectServer';
export const QueryToolContext = React.createContext();
export const QueryToolConnectionContext = React.createContext();
@ -372,8 +372,10 @@ export default function QueryToolComponent({params, pgWindow, pgAdmin, selectedN
eventBus.current.fireEvent(QUERY_TOOL_EVENTS.HANDLE_API_ERROR, kberr);
});
} else if(error?.response?.status == 428) {
connectServerModal(error.response?.data?.result, (passwordData)=>{
initializeQueryTool(passwordData.password);
connectServerModal(modal, error.response?.data?.result, async (passwordData)=>{
await connectServer(api, modal, selectedConn.sid, selectedConn.user, passwordData, async ()=>{
initializeQueryTool();
});
}, ()=>{
setQtStatePartial({
connected: false,
@ -533,7 +535,7 @@ export default function QueryToolComponent({params, pgWindow, pgAdmin, selectedN
}).then(()=>{
initializeQueryTool();
}).catch((err)=>{
eventBus.fireEvent(QUERY_TOOL_EVENTS.HANDLE_API_ERROR, err);
eventBus.current.fireEvent(QUERY_TOOL_EVENTS.HANDLE_API_ERROR, err);
});
} else if(error.response?.status == 403 && error.response?.data.info == 'ACCESS_DENIED') {
pgAdmin.Browser.notifier.error(error.response.data.errormsg);
@ -617,25 +619,6 @@ export default function QueryToolComponent({params, pgWindow, pgAdmin, selectedN
eventBus.current.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_QUERY_CHANGE);
}, [qtState.params.title]);
const connectServerModal = async (modalData, connectCallback, cancelCallback) => {
modal.showModal(gettext('Connect to server'), (closeModal)=>{
return (
<ConnectServerContent
closeModal={()=>{
cancelCallback?.();
closeModal();
}}
data={modalData}
onOK={(formData)=>{
connectCallback(Object.fromEntries(formData));
closeModal();
}}
/>
);
}, {
onClose: cancelCallback,
});
};
const updateQueryToolConnection = (connectionData, isNew=false)=>{
let currSelectedConn = _.find(qtState.connection_list, (c)=>c.is_selected);
@ -707,7 +690,7 @@ export default function QueryToolComponent({params, pgWindow, pgAdmin, selectedN
})
.catch((error)=>{
if(error?.response?.status == 428) {
connectServerModal(error.response?.data?.result, (passwordData)=>{
connectServerModal(modal, error.response?.data?.result, (passwordData)=>{
resolve(
updateQueryToolConnection({
...connectionData,

View File

@ -0,0 +1,55 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2025, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import React from 'react';
import gettext from 'sources/gettext';
import url_for from 'sources/url_for';
import ConnectServerContent from '../../../../../static/js/Dialogs/ConnectServerContent';
export function connectServerModal(modal, modalData, connectCallback, cancelCallback) {
modal.showModal(gettext('Connect to server'), (closeModal)=>{
return (
<ConnectServerContent
closeModal={()=>{
cancelCallback?.();
closeModal();
}}
data={modalData}
onOK={(formData)=>{
connectCallback(Object.fromEntries(formData));
closeModal();
}}
/>
);
}, {
onClose: cancelCallback,
});
}
export async function connectServer(api, modal, sid, user, formData, connectCallback) {
try {
let {data: respData} = await api({
method: 'POST',
url: url_for('sqleditor.connect_server', {
'sid': sid,
...(user ? {
'usr': user,
}:{}),
}),
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
data: formData
});
connectCallback?.(respData.data);
} catch (error) {
connectServerModal(modal, error.response?.data?.result, async (data)=>{
connectServer(api, modal, sid, user, data, connectCallback);
}, ()=>{
/*This is intentional (SonarQube)*/
});
}
}

View File

@ -30,7 +30,7 @@ import EmptyPanelMessage from '../../../../../../static/js/components/EmptyPanel
import { GraphVisualiser } from './GraphVisualiser';
import { usePgAdmin } from '../../../../../../static/js/PgAdminProvider';
import pgAdmin from 'sources/pgadmin';
import ConnectServerContent from '../../../../../../static/js/Dialogs/ConnectServerContent';
import { connectServer, connectServerModal } from '../connectServer';
const StyledBox = styled(Box)(({theme}) => ({
display: 'flex',
@ -190,47 +190,6 @@ export class ResultSetUtils {
);
}
}
connectServerModal (modalData, connectCallback, cancelCallback) {
this.queryToolCtx.modal.showModal(gettext('Connect to server'), (closeModal)=>{
return (
<ConnectServerContent
closeModal={()=>{
cancelCallback?.();
closeModal();
}}
data={modalData}
onOK={(formData)=>{
connectCallback(Object.fromEntries(formData));
closeModal();
}}
/>
);
}, {
onClose: cancelCallback,
});
};
async connectServer (sid, user, formData, connectCallback) {
try {
let {data: respData} = await this.api({
method: 'POST',
url: url_for('sqleditor.connect_server', {
'sid': sid,
...(user ? {
'usr': user,
}:{}),
}),
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
data: formData
});
connectCallback?.(respData.data);
} catch (error) {
this.connectServerModal(error.response?.data?.result, async (data)=>{
this.connectServer(sid, user, data, connectCallback);
}, ()=>{
/*This is intentional (SonarQube)*/
});
}
};
async startExecution(query, explainObject, macroSQL, onIncorrectSQL, flags={
isQueryTool: true, external: false, reconnect: false, executeCursor: false, refreshData: false,
@ -294,8 +253,8 @@ export class ResultSetUtils {
}
} catch(e) {
if(e?.response?.status == 428){
this.connectServerModal(e.response?.data?.result, async (passwordData)=>{
await this.connectServer(this.queryToolCtx.params.sid, this.queryToolCtx.params.user, passwordData, async ()=>{
connectServerModal(this.queryToolCtx.modal, e.response?.data?.result, async (passwordData)=>{
await connectServer(this.api, this.queryToolCtx.modal, this.queryToolCtx.params.sid, this.queryToolCtx.params.user, passwordData, async ()=>{
await this.eventBus.fireEvent(QUERY_TOOL_EVENTS.REINIT_QT_CONNECTION, '', explainObject, macroSQL, flags.executeCursor, true);
});
}, ()=>{
@ -304,7 +263,7 @@ export class ResultSetUtils {
}else if (e?.response?.data.info == 'CRYPTKEY_MISSING'){
let pgBrowser = window.pgAdmin.Browser;
pgBrowser.set_master_password('', async (passwordData)=>{
await this.connectServer(this.queryToolCtx.params.sid, this.queryToolCtx.params.user, passwordData, async ()=>{
await connectServer(this.api, this.queryToolCtx.modal, this.queryToolCtx.params.sid, this.queryToolCtx.params.user, passwordData, async ()=>{
await this.eventBus.fireEvent(QUERY_TOOL_EVENTS.REINIT_QT_CONNECTION, '', explainObject, macroSQL, flags.executeCursor, true);
});
}, ()=> {
@ -463,11 +422,14 @@ export class ResultSetUtils {
if (error.response?.status === 428) {
// Handle 428: Show password dialog.
return new Promise((resolve, reject) => {
this.connectServerModal(
connectServerModal(
this.queryToolCtx.modal,
error.response?.data?.result,
async (formData) => {
try {
await this.connectServer(
await connectServer(
this.api,
this.queryToolCtx.modal,
this.queryToolCtx.params.sid,
this.queryToolCtx.params.user,
formData,