diff --git a/docs/en_US/preferences.rst b/docs/en_US/preferences.rst index 1a5f4fc60..64d73c267 100644 --- a/docs/en_US/preferences.rst +++ b/docs/en_US/preferences.rst @@ -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, diff --git a/web/pgadmin/static/js/ToolErrorView.jsx b/web/pgadmin/static/js/ToolErrorView.jsx index 4db6b1126..da7a9233a 100644 --- a/web/pgadmin/static/js/ToolErrorView.jsx +++ b/web/pgadmin/static/js/ToolErrorView.jsx @@ -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 ( {HTMLReactParse(err_msg)} ); @@ -33,4 +37,5 @@ ToolErrorView.propTypes = { error: PropTypes.string, panelId: PropTypes.string, panelDocker: PropTypes.object, + toolDataId: PropTypes.string }; \ No newline at end of file diff --git a/web/pgadmin/tools/erd/__init__.py b/web/pgadmin/tools/erd/__init__.py index 0a9eb8c96..e9aed64a3 100644 --- a/web/pgadmin/tools/erd/__init__.py +++ b/web/pgadmin/tools/erd/__init__.py @@ -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) diff --git a/web/pgadmin/tools/erd/static/js/ERDModule.js b/web/pgadmin/tools/erd/static/js/ERDModule.js index 0c8d3a5aa..9a1c8b6e3 100644 --- a/web/pgadmin/tools/erd/static/js/ERDModule.js +++ b/web/pgadmin/tools/erd/static/js/ERDModule.js @@ -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')) ); diff --git a/web/pgadmin/tools/erd/static/js/erd_tool/components/ConnectionBar.jsx b/web/pgadmin/tools/erd/static/js/erd_tool/components/ConnectionBar.jsx index 64f227817..a35e02d4a 100644 --- a/web/pgadmin/tools/erd/static/js/erd_tool/components/ConnectionBar.jsx +++ b/web/pgadmin/tools/erd/static/js/erd_tool/components/ConnectionBar.jsx @@ -42,7 +42,7 @@ export const STATUS = { function ConnectionStatusIcon({status}) { if(status == STATUS.CONNECTING) { return ; - } else if(status == STATUS.CONNECTED || status == STATUS.FAILED) { + } else if(status == STATUS.CONNECTED) { return ; } else { return ; diff --git a/web/pgadmin/tools/erd/static/js/erd_tool/components/ERDTool.jsx b/web/pgadmin/tools/erd/static/js/erd_tool/components/ERDTool.jsx index 4db86dd34..41050fc98 100644 --- a/web/pgadmin/tools/erd/static/js/erd_tool/components/ERDTool.jsx +++ b/web/pgadmin/tools/erd/static/js/erd_tool/components/ERDTool.jsx @@ -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, diff --git a/web/pgadmin/tools/psql/__init__.py b/web/pgadmin/tools/psql/__init__.py index c50d05a8a..6d7008922 100644 --- a/web/pgadmin/tools/psql/__init__.py +++ b/web/pgadmin/tools/psql/__init__.py @@ -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 diff --git a/web/pgadmin/tools/psql/static/js/PsqlModule.js b/web/pgadmin/tools/psql/static/js/PsqlModule.js index 8b49e92ec..2a8a8e6be 100644 --- a/web/pgadmin/tools/psql/static/js/PsqlModule.js +++ b/web/pgadmin/tools/psql/static/js/PsqlModule.js @@ -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', diff --git a/web/pgadmin/tools/sqleditor/__init__.py b/web/pgadmin/tools/sqleditor/__init__.py index 981dfbe7c..25cdf624b 100644 --- a/web/pgadmin/tools/sqleditor/__init__.py +++ b/web/pgadmin/tools/sqleditor/__init__.py @@ -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, diff --git a/web/pgadmin/tools/sqleditor/static/js/SQLEditorModule.js b/web/pgadmin/tools/sqleditor/static/js/SQLEditorModule.js index f73041e1f..609cc545c 100644 --- a/web/pgadmin/tools/sqleditor/static/js/SQLEditorModule.js +++ b/web/pgadmin/tools/sqleditor/static/js/SQLEditorModule.js @@ -254,6 +254,7 @@ export default class SQLEditor { error={params.error} panelId={`${BROWSER_PANELS.QUERY_TOOL}_${params.trans_id}`} panelDocker={panelDocker} + toolDataId={params.toolDataId} /> : { - 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 ( - { - 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, diff --git a/web/pgadmin/tools/sqleditor/static/js/components/connectServer.js b/web/pgadmin/tools/sqleditor/static/js/components/connectServer.js new file mode 100644 index 000000000..6351958a3 --- /dev/null +++ b/web/pgadmin/tools/sqleditor/static/js/components/connectServer.js @@ -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 ( + { + 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)*/ + }); + } +} \ No newline at end of file diff --git a/web/pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx b/web/pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx index 6c17b7e0e..3a1778933 100644 --- a/web/pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx +++ b/web/pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx @@ -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 ( - { - 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,