Added support to preserve the workspace, query windows, and pgAdmin state during an abrupt shutdown or restart. #3319

pull/8835/head
Yogesh Mahajan 2025-06-05 16:50:38 +05:30 committed by GitHub
parent c2ef9d06ca
commit 68e559c613
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
44 changed files with 995 additions and 253 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 280 KiB

After

Width:  |  Height:  |  Size: 294 KiB

View File

@ -309,12 +309,21 @@ Use the fields on the *User Interface* panel to set the user interface related p
this setting is False, meaning that Query Tool/PSQL tabs will open in the currently
active workspace (either the default or the workspace selected at the time of opening).
* When the *Save the application state?* option is enabled the current state of various
tools—such as Query Tool, ERD, Schema Diff, and PSQL—will be saved in the encrypted
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:**
* 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,
check `here <https://github.com/pgadmin-org/pgadmin4/blob/master/README.md>`_ how.
Currently we support Light, Dark, High Contrast and System theme. Selecting System option will follow
your computer's settings.
**Note:** Saving the application state will not preserve data for tool tabs opened in
separate browser tabs when running in server mode..
The Paths Node
**************

View File

@ -40,6 +40,16 @@ def upgrade():
))
)
op.create_table(
'application_state',
sa.Column('uid', sa.Integer(), nullable=False),
sa.Column('id', sa.Integer()),
sa.Column('connection_info', sa.JSON()),
sa.Column('tool_name', sa.String(length=64)),
sa.Column('tool_data', sa.String()),
sa.ForeignKeyConstraint(['uid'], ['user.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id', 'uid'))
def downgrade():
# pgAdmin only upgrades, downgrade not implemented.

View File

@ -292,7 +292,6 @@ class AuthSourceManager:
current_app.logger.debug(
"Authentication initiated via source: %s is failed." %
source.get_source_name())
return status, msg
def login(self):

View File

@ -12,9 +12,11 @@ import _ from 'lodash';
import { checkMasterPassword, showQuickSearch } from '../../../static/js/Dialogs/index';
import { pgHandleItemError } from '../../../static/js/utils';
import { send_heartbeat, stop_heartbeat } from './heartbeat';
import getApiInstance from '../../../static/js/api_instance';
import getApiInstance, {parseApiError} from '../../../static/js/api_instance';
import usePreferences, { setupPreferenceBroadcast } from '../../../preferences/static/js/store';
import checkNodeVisibility from '../../../static/js/check_node_visibility';
import * as showQueryTool from '../../../tools/sqleditor/static/js/show_query_tool';
import {getRandomInt} from 'sources/utils';
define('pgadmin.browser', [
'sources/gettext', 'sources/url_for', 'sources/pgadmin',
@ -206,6 +208,12 @@ define('pgadmin.browser', [
uiloaded: function() {
this.set_master_password('');
this.check_version_update();
const prefStore = usePreferences.getState();
let save_the_workspace = prefStore.getPreferencesForModule('misc').save_app_state;
if(save_the_workspace){
this.restore_pgadmin_state();
pgBrowser.docker.default_workspace.focus();
}
},
check_corrupted_db_file: function() {
getApiInstance().get(
@ -291,6 +299,42 @@ define('pgadmin.browser', [
});
},
restore_pgadmin_state: function () {
getApiInstance({'Content-Encoding': 'gzip'}).get(
url_for('settings.get_application_state')
).then((res)=> {
if(res.data.success && res.data.data.result.length > 0){
_.each(res.data.data.result, function(toolState){
let toolNme = toolState.tool_name;
let toolDataId = `${toolNme}-${getRandomInt(1, 9999999)}`;
let connectionInfo = toolState.connection_info;
localStorage.setItem(toolDataId, toolState.tool_data);
if (toolNme == 'sqleditor'){
showQueryTool.relaunchSqlTool(connectionInfo, toolDataId);
}else if(toolNme == 'psql'){
pgAdmin.Tools.Psql.openPsqlTool(null, null, connectionInfo);
}else if(toolNme == 'ERD'){
pgAdmin.Tools.ERD.showErdTool(null, null, false, connectionInfo, toolDataId);
}else if(toolNme == 'schema_diff'){
pgAdmin.Tools.SchemaDiff.launchSchemaDiff(toolDataId);
}
});
// call clear application state data.
try {
getApiInstance().delete(url_for('settings.delete_application_state'), {});
} catch (error) {
console.error(error);
pgAdmin.Browser.notifier.error(gettext('Failed to remove query data.') + parseApiError(error));
}
}
}).catch(function(error) {
pgAdmin.Browser.notifier.pgRespErrorNotify(error);
getApiInstance().delete(url_for('settings.delete_application_state'), {});
});
},
bind_beforeunload: function() {
window.addEventListener('beforeunload', function(e) {
/* Can open you in new tab */

View File

@ -124,6 +124,19 @@ class MiscModule(PgAdminModule):
)
)
self.preference.register(
'user_interface', 'save_app_state',
gettext("Save the application state?"),
'boolean', True,
category_label=PREF_LABEL_USER_INTERFACE,
help_str=gettext(
'If set to True, pgAdmin will save the state of opened tools'
' (such as Query Tool, PSQL, Schema Diff, and ERD), including'
' any unsaved data. This data will be automatically restored'
' in the event of an unexpected shutdown or browser refresh.'
)
)
if not config.SERVER_MODE:
self.preference.register(
'file_downloads', 'automatically_open_downloaded_file',

View File

@ -455,7 +455,7 @@ export default function AdHocConnection({mode}) {
'pgadmin:tool:show',
`${BROWSER_PANELS.PSQL_TOOL}_${transId}`,
openUrl,
{title: escapedTitle, db: db_name},
{title: escapedTitle, db: db_name, server_name: formData.server_name, 'user': user_name},
{title: panelTitle, icon: 'pg-font-icon icon-terminal', manualClose: false, renamable: true},
Boolean(open_new_tab?.includes('psql_tool'))
);

View File

@ -75,16 +75,16 @@ export function WorkspaceProvider({children}) {
pgAdmin.Browser.docker.currentWorkspace = newVal;
if (newVal == WORKSPACES.DEFAULT) {
setTimeout(() => {
pgAdmin.Browser.tree.selectNode(lastSelectedTreeItem.current, true, 'center');
pgAdmin.Browser.tree?.selectNode(lastSelectedTreeItem.current, true, 'center');
lastSelectedTreeItem.current = null;
}, 250);
} else {
// Get the selected tree node and save it into the state variable.
let selItem = pgAdmin.Browser.tree.selected();
let selItem = pgAdmin.Browser.tree?.selected();
if (selItem)
lastSelectedTreeItem.current = selItem;
// Deselect the node to disable the menu options.
pgAdmin.Browser.tree.deselect(selItem);
pgAdmin.Browser.tree?.deselect(selItem);
}
setCurrentWorkspace(newVal);
};

View File

@ -392,6 +392,17 @@ class QueryHistoryModel(db.Model):
last_updated_flag = db.Column(db.String(), nullable=False)
class ApplicationState(db.Model):
"""Define the application state SQL table."""
__tablename__ = 'application_state'
uid = db.Column(db.Integer(), db.ForeignKey(USER_ID), nullable=False,
primary_key=True)
id = db.Column(db.Integer(),nullable=False,primary_key=True)
connection_info = db.Column(MutableDict.as_mutable(types.JSON))
tool_name = db.Column(db.String(64), nullable=False)
tool_data = db.Column(PgAdminDbBinaryString())
class Database(db.Model):
"""
Define a Database.

View File

@ -16,6 +16,8 @@ import config
import json
from flask import render_template, Response, request, session, current_app
from flask_babel import gettext
from pgadmin.settings import delete_tool_data
from pgadmin.user_login_check import pga_login_required
from pgadmin.utils import PgAdminModule
from pgadmin.utils.ajax import success_return, \
@ -238,6 +240,9 @@ def save():
data['mid'], data['category_id'], data['id'], data['value'])
sgm.get_nodes(sgm)
if data['name'] == 'save_app_state' and not data['value']:
delete_tool_data()
if not res:
return internal_server_error(errormsg=msg)

View File

@ -8,22 +8,24 @@
##########################################################################
"""Utility functions for storing and retrieving user configuration settings."""
import traceback
import os
import json
from flask import Response, request, render_template, url_for, current_app
from flask import Response, request, render_template, current_app
from flask_babel import gettext
from flask_login import current_user
from pgadmin.user_login_check import pga_login_required
from pgadmin.utils import PgAdminModule
from pgadmin.utils import PgAdminModule, get_complete_file_path
from pgadmin.utils.ajax import make_json_response, bad_request,\
success_return, internal_server_error
from pgadmin.utils.menu import MenuItem
from pgadmin.model import db, Setting
from pgadmin.model import db, Setting, ApplicationState
from pgadmin.utils.constants import MIMETYPE_APP_JS
from .utils import get_dialog_type, get_file_type_setting
from cryptography.fernet import Fernet
import hashlib
MODULE_NAME = 'settings'
@ -52,7 +54,10 @@ class SettingsModule(PgAdminModule):
'settings.save_tree_state', 'settings.get_tree_state',
'settings.reset_tree_state',
'settings.save_file_format_setting',
'settings.get_file_format_setting'
'settings.get_file_format_setting',
'settings.save_application_state',
'settings.get_application_state',
'settings.delete_application_state'
]
@ -256,3 +261,170 @@ def get_file_format_setting():
return make_json_response(success=True,
info=get_file_type_setting(list(data.values())))
@blueprint.route(
'/save_application_state',
methods=["POST"], endpoint='save_application_state'
)
@pga_login_required
def save_application_state():
"""
Expose an api to save the application state which stores the data from
query tool, ERD, schema-diff, psql
"""
data = json.loads(request.data)
trans_id = data['trans_id']
fernet = Fernet(current_app.config['SECRET_KEY'].encode())
tool_data = fernet.encrypt(json.dumps(data['tool_data']).encode())
connection_info = data['connection_info'] \
if 'connection_info' in data else None
if ('open_file_name' in connection_info and
connection_info['open_file_name']):
file_path = get_complete_file_path(connection_info['open_file_name'])
connection_info['last_saved_file_hash'] = (
get_last_saved_file_hash(file_path, trans_id))
try:
data_entry = ApplicationState(
uid=current_user.id, id=trans_id,connection_info=connection_info,
tool_name=data['tool_name'], tool_data=tool_data)
db.session.merge(data_entry)
db.session.commit()
except Exception as e:
print(e)
db.session.rollback()
return make_json_response(
data={
'status': True,
'msg': 'Success',
})
def get_last_saved_file_hash(file_path, trans_id):
result = db.session \
.query(ApplicationState) \
.filter(ApplicationState.uid == current_user.id,
ApplicationState.id == trans_id).all()
file_hash_update_require = True
last_saved_file_hash = None
for row in result:
connection_info = row.connection_info
if ('open_file_name' in connection_info and
connection_info['open_file_name']):
file_hash_update_require = not connection_info['is_editor_dirty']
last_saved_file_hash = connection_info['last_saved_file_hash']
if file_hash_update_require:
last_saved_file_hash = compute_md5_hash_file(file_path)
return last_saved_file_hash
@blueprint.route(
'/get_application_state',
methods=["GET"], endpoint='get_application_state'
)
@pga_login_required
def get_application_state():
"""
Returns application state if any stored.
"""
fernet = Fernet(current_app.config['SECRET_KEY'].encode())
result = db.session \
.query(ApplicationState) \
.filter(ApplicationState.uid == current_user.id) \
.all()
res = []
for row in result:
connection_info = row.connection_info
if ('open_file_name' in connection_info and
connection_info['open_file_name']):
file_path = get_complete_file_path(
connection_info['open_file_name'])
file_deleted = False if os.path.exists(file_path) else True
connection_info['file_deleted'] = file_deleted
if (not file_deleted and connection_info['is_editor_dirty'] and
'last_saved_file_hash' in connection_info and
connection_info['last_saved_file_hash']):
connection_info['external_file_changes'] = \
check_external_file_changes(
file_path, connection_info['last_saved_file_hash'])
res.append({'tool_name': row.tool_name,
'connection_info': connection_info,
'tool_data': fernet.decrypt(row.tool_data).decode(),
'id': row.id
})
return make_json_response(
data={
'status': True,
'msg': '',
'result': res
}
)
@blueprint.route(
'/delete_application_state/',
methods=["DELETE"], endpoint='delete_application_state')
@pga_login_required
def delete_application_state():
trans_id = None
if request.data:
data = json.loads(request.data)
trans_id = int(data['panelId'].split('_')[-1])
status, msg = delete_tool_data(trans_id)
return make_json_response(
data={
'status': status,
'msg': msg,
}
)
def delete_tool_data(trans_id=None):
try:
if trans_id:
results = db.session \
.query(ApplicationState) \
.filter(ApplicationState.uid == current_user.id,
ApplicationState.id == trans_id) \
.all()
else:
results = db.session \
.query(ApplicationState) \
.filter(ApplicationState.uid == current_user.id) \
.all()
for result in results:
db.session.delete(result)
db.session.commit()
return True, 'Success'
except Exception as e:
db.session.rollback()
return False, str(e)
def compute_md5_hash_file(file_path, chunk_size=8192):
"""Compute md5 hash for large files by reading in chunks."""
md5_hash = hashlib.md5()
# Open the file in binary mode
with open(file_path, "rb") as file:
# Read and hash in 8 KB chunks (can adjust the chunk size if needed)
for chunk in iter(lambda: file.read(chunk_size), b""):
md5_hash.update(chunk)
return md5_hash.hexdigest()
def check_external_file_changes(file_path, last_saved_file_hash):
current_file_hash = compute_md5_hash_file(file_path)
if current_file_hash != last_saved_file_hash:
return True
return False

View File

@ -0,0 +1,79 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2025, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import React, { useContext, useMemo } from 'react';
import PropTypes from 'prop-types';
import getApiInstance from '../../static/js/api_instance';
import url_for from 'sources/url_for';
import { getBrowser } from '../../static/js/utils';
import usePreferences from '../../preferences/static/js/store';
import pgAdmin from 'sources/pgadmin';
const ApplicationStateContext = React.createContext();
export const useApplicationState = ()=>useContext(ApplicationStateContext);
export function getToolData(localStorageId){
let toolDataJson = JSON.parse(localStorage.getItem(localStorageId));
localStorage.removeItem(localStorageId);
return toolDataJson;
}
export function deleteToolData(panelId, closePanelId){
const saveAppState = usePreferences.getState().getPreferencesForModule('misc')?.save_app_state;
if(saveAppState){
if(panelId == closePanelId){
let api = getApiInstance();
api.delete(
url_for('settings.delete_application_state'), {data:{'panelId': panelId}}
).then(()=> { /* Sonar Qube */}).catch(function(error) {
pgAdmin.Browser.notifier.pgRespErrorNotify(error);
});
}
}
};
export function ApplicationStateProvider({children}){
const preferencesStore = usePreferences();
const saveAppState = preferencesStore?.getPreferencesForModule('misc')?.save_app_state;
const openNewTab = preferencesStore?.getPreferencesForModule('browser')?.new_browser_tab_open;
const saveToolData = (toolName, connectionInfo, transId, toolData) =>{
let data = {
'tool_name': toolName,
'connection_info': connectionInfo,
'trans_id': transId,
'tool_data': toolData
};
getApiInstance({'Content-Encoding': 'gzip'}).post(
url_for('settings.save_application_state'),
JSON.stringify(data),
).catch((error)=>{console.error(error);});
};
const isSaveToolDataEnabled = (toolName)=>{
let toolMapping = {'sqleditor': 'qt', 'schema_diff': 'schema_diff', 'psql': 'psql_tool', 'ERD': 'erd_tool'};
if(openNewTab?.includes(toolMapping[toolName])){
return saveAppState && getBrowser().name == 'Electron';
}
return saveAppState;
};
const value = useMemo(()=>({
saveToolData,
isSaveToolDataEnabled,
}), []);
return <ApplicationStateContext.Provider value={value}>
{children}
</ApplicationStateContext.Provider>;
}
ApplicationStateProvider.propTypes = {
children: PropTypes.object
};

View File

@ -18,7 +18,6 @@ export function usePgAdmin() {
}
export function PgAdminProvider({children, value}) {
return <PgAdminContext.Provider value={value}>
{children}
</PgAdminContext.Provider>;

View File

@ -0,0 +1,39 @@
import PropTypes from 'prop-types';
import React from 'react';
import gettext from 'sources/gettext';
import { LAYOUT_EVENTS } from './helpers/Layout';
import { styled } from '@mui/material/styles';
import { Box } from '@mui/material';
import { FormHelperText } from '@mui/material';
import HTMLReactParse from 'html-react-parser';
const StyledBox = styled(Box)(({theme}) => ({
color: theme.palette.text.primary,
margin: '24px auto 12px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: '100%',
}));
export default function ToolErrorView({error, panelId, panelDocker}){
panelDocker.eventBus.registerListener(LAYOUT_EVENTS.CLOSING, (id)=>{
if(panelId == id) {
panelDocker.close(panelId, true);
}
});
let err_msg = gettext(`There was some error while opening: ${error}`);
return (<StyledBox>
<FormHelperText variant="outlined" error= {true} style={{marginLeft: '4px'}} >{HTMLReactParse(err_msg)}</FormHelperText>
</StyledBox>);
}
ToolErrorView.propTypes = {
error: PropTypes.string,
panelId: PropTypes.string,
panelDocker: PropTypes.object,
pgAdmin: PropTypes.object,
toolName: PropTypes.string,
};

View File

@ -13,6 +13,9 @@ import { usePgAdmin } from './PgAdminProvider';
import { BROWSER_PANELS } from '../../browser/static/js/constants';
import PropTypes from 'prop-types';
import LayoutIframeTab from './helpers/Layout/LayoutIframeTab';
import { LAYOUT_EVENTS } from './helpers/Layout';
import { deleteToolData } from '../../settings/static/ApplicationStateProvider';
function ToolForm({actionUrl, params}) {
const formRef = useRef(null);
@ -56,6 +59,11 @@ export default function ToolView({dockerObj}) {
// Handler here will return which layout instance the tool should go in
// case of workspace layout.
let handler = pgAdmin.Browser.getDockerHandler?.(panelId, dockerObj);
const deregisterRemove = handler.docker.eventBus.registerListener(LAYOUT_EVENTS.REMOVE, (closePanelId)=>{
deleteToolData(panelId, closePanelId);
deregisterRemove();
});
handler.focus();
handler.docker.openTab({
id: panelId,

View File

@ -26,6 +26,7 @@ import usePreferences from '../../../../preferences/static/js/store';
import _ from 'lodash';
import UtilityView from '../../UtilityView';
import ToolView from '../../ToolView';
import { ApplicationStateProvider } from '../../../../settings/static/ApplicationStateProvider';
function TabTitle({id, closable, defaultInternal}) {
const layoutDocker = React.useContext(LayoutDockerContext);
@ -497,7 +498,9 @@ export default function Layout({groups, noContextGroups, getLayoutInstance, layo
label="Layout Context Menu" />
{enableToolEvents && <>
<UtilityView dockerObj={layoutDockerObj} />
<ToolView dockerObj={layoutDockerObj} />
<ApplicationStateProvider>
<ToolView dockerObj={layoutDockerObj} />
</ApplicationStateProvider>
</>}
</LayoutDockerContext.Provider>
);

View File

@ -10,7 +10,7 @@
"""A blueprint module implementing the erd tool."""
import json
from flask import url_for, request, Response
from flask import request, Response
from flask import render_template, current_app as app
from flask_security import permissions_required
from pgadmin.user_login_check import pga_login_required
@ -18,8 +18,7 @@ from flask_babel import gettext
from werkzeug.user_agent import UserAgent
from pgadmin.utils import PgAdminModule, \
SHORTCUT_FIELDS as shortcut_fields
from pgadmin.utils.ajax import make_json_response, bad_request, \
internal_server_error
from pgadmin.utils.ajax import make_json_response, internal_server_error
from pgadmin.model import Server
from config import PG_DEFAULT_DRIVER
from pgadmin.utils.driver import get_driver
@ -29,13 +28,14 @@ from pgadmin.browser.server_groups.servers.databases.schemas.utils \
from pgadmin.browser.server_groups.servers.databases.schemas.tables. \
constraints.foreign_key import utils as fkey_utils
from pgadmin.utils.constants import PREF_LABEL_KEYBOARD_SHORTCUTS, \
PREF_LABEL_DISPLAY, PREF_LABEL_OPTIONS
PREF_LABEL_OPTIONS
from .utils import ERDHelper
from pgadmin.utils.exception import ConnectionLost
from pgadmin.authenticate import socket_login_required
from pgadmin.tools.user_management.PgAdminPermissions import AllPermissionTypes
from ... import socketio
MODULE_NAME = 'erd'
SOCKETIO_NAMESPACE = '/{0}'.format(MODULE_NAME)
@ -462,11 +462,11 @@ def panel(trans_id):
Args:
panel_title: Title of the panel
"""
params = {'trans_id': trans_id, }
if request.form:
for key, val in request.form.items():
params[key] = val
params = {
'trans_id': trans_id,
'title': request.form['title']
}
if request.args:
params.update({k: v for k, v in request.args.items()})
@ -500,19 +500,26 @@ def panel(trans_id):
s = Server.query.filter_by(id=int(params['sid'])).first()
params.update({
'bgcolor': s.bgcolor,
'fgcolor': s.fgcolor,
'client_platform': user_agent.platform,
'is_desktop_mode': app.PGADMIN_RUNTIME,
'is_linux': is_linux_platform
})
if s:
params.update({
'bgcolor': s.bgcolor,
'fgcolor': s.fgcolor,
'client_platform': user_agent.platform,
'is_desktop_mode': app.PGADMIN_RUNTIME,
'is_linux': is_linux_platform
})
return render_template(
"erd/index.html",
title=underscore_unescape(params['title']),
params=json.dumps(params),
)
return render_template(
"erd/index.html",
title=underscore_unescape(params['title']),
params=json.dumps(params),
)
else:
params['error'] = 'Server did not find.'
return render_template(
"erd/index.html",
title=None,
params=json.dumps(params))
@blueprint.route(

View File

@ -21,6 +21,8 @@ import { NotifierProvider } from '../../../../static/js/helpers/Notifier';
import usePreferences, { listenPreferenceBroadcast } from '../../../../preferences/static/js/store';
import pgAdmin from 'sources/pgadmin';
import { PgAdminProvider } from '../../../../static/js/PgAdminProvider';
import { ApplicationStateProvider } from '../../../../settings/static/ApplicationStateProvider';
import ToolErrorView from '../../../../static/js/ToolErrorView';
export function setPanelTitle(docker, panelId, panelTitle) {
docker.setTitle(panelId, panelTitle);
@ -86,27 +88,53 @@ export default class ERDModule {
}
// Callback to draw ERD Tool for objects
showErdTool(_data, treeIdentifier, gen=false) {
if (treeIdentifier === undefined) {
pgAdmin.Browser.notifier.alert(
gettext('ERD Error'),
gettext('No object selected.')
);
return;
}
showErdTool(_data, treeIdentifier, gen=false, connectionInfo=null, toolDataId=null) {
let parentData = null;
let panelTitle = null;
if(connectionInfo){
panelTitle = connectionInfo.title;
parentData = {
server_group: {
_id: connectionInfo.sgid || 0,
server_type: connectionInfo.server_type
},
server: {
_id: connectionInfo.sid,
},
database: {
_id: connectionInfo.did,
},
schema: {
_id: connectionInfo.scid || null,
},
table: {
_id: connectionInfo.tid || null,
}
};
const parentData = this.pgBrowser.tree.getTreeNodeHierarchy(treeIdentifier);
}else{
if (treeIdentifier === undefined) {
pgAdmin.Browser.notifier.alert(
gettext('ERD Error'),
gettext('No object selected.')
);
return;
}
parentData = this.pgBrowser.tree.getTreeNodeHierarchy(treeIdentifier);
if(_.isUndefined(parentData.database)) {
pgAdmin.Browser.notifier.alert(
gettext('ERD Error'),
gettext('Please select a database/database object.')
);
return;
if(_.isUndefined(parentData.database)) {
pgAdmin.Browser.notifier.alert(
gettext('ERD Error'),
gettext('Please select a database/database object.')
);
return;
}
panelTitle = getPanelTitle(this.pgBrowser, treeIdentifier);
}
const transId = getRandomInt(1, 9999999);
const panelTitle = getPanelTitle(this.pgBrowser, treeIdentifier);
const panelUrl = this.getPanelUrl(transId, parentData, gen);
const open_new_tab = usePreferences.getState().getPreferencesForModule('browser').new_browser_tab_open;
@ -114,7 +142,7 @@ export default class ERDModule {
'pgadmin:tool:show',
`${BROWSER_PANELS.ERD_TOOL}_${transId}`,
panelUrl,
{title: _.escape(panelTitle)},
{sql_id: toolDataId, title: _.escape(panelTitle)},
{title: 'Untitled', icon: 'fa fa-sitemap'},
Boolean(open_new_tab?.includes('erd_tool'))
);
@ -149,18 +177,26 @@ export default class ERDModule {
root.render(
<Theme>
<PgAdminProvider value={pgAdmin}>
<ModalProvider>
<NotifierProvider pgAdmin={this.pgAdmin} pgWindow={pgWindow} />
<ERDTool
params={params}
pgWindow={pgWindow}
pgAdmin={this.pgAdmin}
panelId={`${BROWSER_PANELS.ERD_TOOL}_${params.trans_id}`}
panelDocker={pgWindow.pgAdmin.Browser.docker.default_workspace}
/>
</ModalProvider>
<ApplicationStateProvider>
<ModalProvider>
<NotifierProvider pgAdmin={this.pgAdmin} pgWindow={pgWindow} />
{ params.error ?
<ToolErrorView
error={params.error}
panelId={`${BROWSER_PANELS.ERD_TOOL}_${params.trans_id}`}
panelDocker={pgWindow.pgAdmin.Browser.docker.default_workspace}
/> :
<ERDTool
params={params}
pgWindow={pgWindow}
pgAdmin={this.pgAdmin}
panelId={`${BROWSER_PANELS.ERD_TOOL}_${params.trans_id}`}
panelDocker={pgWindow.pgAdmin.Browser.docker.default_workspace}
/> }
</ModalProvider>
</ApplicationStateProvider>
</PgAdminProvider>
</Theme>
);
}
}
}

View File

@ -20,5 +20,5 @@ export const ERD_EVENTS = {
ZOOM_OUT: 'ZOOM_OUT',
SINGLE_NODE_SELECTED: 'SINGLE_NODE_SELECTED',
ANY_ITEM_SELECTED: 'ANY_ITEM_SELECTED',
DIRTY: 'DIRTY',
DIRTY: 'DIRTY'
};

View File

@ -38,6 +38,7 @@ import { styled } from '@mui/material/styles';
import BeforeUnload from './BeforeUnload';
import { isMac } from '../../../../../../static/js/keyboard_shortcuts';
import DownloadUtils from '../../../../../../static/js/DownloadUtils';
import { getToolData } from '../../../../../../settings/static/ApplicationStateProvider';
/* Custom react-diagram action for keyboard events */
export class KeyboardShortcutAction extends Action {
@ -193,11 +194,11 @@ export default class ERDTool extends React.Component {
},
'linksUpdated': () => {
this.setState({dirty: true});
this.eventBus.fireEvent(ERD_EVENTS.DIRTY, true);
this.eventBus.fireEvent(ERD_EVENTS.DIRTY, true, this.serializeFile());
},
'nodesUpdated': ()=>{
this.setState({dirty: true});
this.eventBus.fireEvent(ERD_EVENTS.DIRTY, true);
this.eventBus.fireEvent(ERD_EVENTS.DIRTY, true, this.serializeFile());
},
'showNote': (event)=>{
this.showNote(event.node);
@ -352,7 +353,16 @@ export default class ERDTool extends React.Component {
done = await this.loadPrequisiteData();
if(!done) return;
if(this.props.params.gen) {
if(this.props.params.sql_id){
let sqlValue = getToolData(this.props.params.sql_id);
if (sqlValue) {
this.diagram.deserialize(sqlValue);
this.diagram.clearSelection();
this.registerModelEvents();
}
}
else if(this.props.params.gen) {
await this.loadTablesData();
}
}
@ -829,6 +839,10 @@ export default class ERDTool extends React.Component {
updated && this.diagram.fireEvent({}, 'nodesUpdated', true);
}
serializeFile(){
return this.diagram.serialize(this.props.pgAdmin.Browser.utils.app_version_int);
}
async initConnection() {
this.setLoading(gettext('Initializing connection...'));
this.setState({conn_status: CONNECT_STATUS.CONNECTING});
@ -928,7 +942,7 @@ export default class ERDTool extends React.Component {
fgcolor={this.props.params.fgcolor} title={_.unescape(this.props.params.title)}/>
<MainToolBar preferences={this.state.preferences} eventBus={this.eventBus}
fillColor={this.state.fill_color} textColor={this.state.text_color}
notation={this.state.cardinality_notation} onNotationChange={this.onNotationChange}
notation={this.state.cardinality_notation} onNotationChange={this.onNotationChange} connectionInfo={this.props.params}
/>
<FloatingNote open={this.state.note_open} onClose={this.onNoteClose}
anchorEl={this.noteRefEle} noteNode={this.state.note_node} appendTo={this.diagramContainerRef.current} rows={8}/>
@ -958,6 +972,7 @@ ERDTool.propTypes = {
bgcolor: PropTypes.string,
fgcolor: PropTypes.string,
gen: PropTypes.bool.isRequired,
sql_id: PropTypes.string,
}),
pgWindow: PropTypes.object.isRequired,
pgAdmin: PropTypes.object.isRequired,

View File

@ -37,6 +37,8 @@ import { ERD_EVENTS } from '../ERDConstants';
import { MagicIcon, SQLFileIcon } from '../../../../../../static/js/components/ExternalIcon';
import { useModal } from '../../../../../../static/js/helpers/ModalProvider';
import { withColorPicker } from '../../../../../../static/js/helpers/withColorPicker';
import { useApplicationState } from '../../../../../../settings/static/ApplicationStateProvider';
import { useDelayDebounce } from '../../../../../../static/js/custom_hooks';
const StyledBox = styled(Box)(({theme}) => ({
padding: '2px 4px',
@ -48,7 +50,7 @@ const StyledBox = styled(Box)(({theme}) => ({
...theme.mixins.panelBorder.bottom,
}));
export function MainToolBar({preferences, eventBus, fillColor, textColor, notation, onNotationChange}) {
export function MainToolBar({preferences, eventBus, fillColor, textColor, notation, onNotationChange, connectionInfo}) {
const theme = useTheme();
const [buttonsDisabled, setButtonsDisabled] = useState({
'save': true,
@ -62,6 +64,7 @@ export function MainToolBar({preferences, eventBus, fillColor, textColor, notati
});
const [showDetails, setShowDetails] = useState(true);
const {saveToolData, isSaveToolDataEnabled} = useApplicationState();
const {openMenuName, toggleMenu, onMenuClose} = usePgMenuGroup();
const saveAsMenuRef = React.useRef(null);
const sqlMenuRef = React.useRef(null);
@ -130,9 +133,12 @@ export function MainToolBar({preferences, eventBus, fillColor, textColor, notati
[ERD_EVENTS.ANY_ITEM_SELECTED, (selected)=>{
setDisableButton('drop-table', !selected);
}],
[ERD_EVENTS.DIRTY, (isDirty)=>{
[ERD_EVENTS.DIRTY, (isDirty, data)=>{
isDirtyRef.current = isDirty;
setDisableButton('save', !isDirty);
if(isDirty && isSaveToolDataEnabled('ERD')){
setSaveERDData(data);
}
}],
];
events.forEach((e)=>{
@ -145,6 +151,11 @@ export function MainToolBar({preferences, eventBus, fillColor, textColor, notati
};
}, []);
const [saveERDData, setSaveERDData] = useState(null);
useDelayDebounce((erdData)=>{
saveToolData('ERD', connectionInfo, connectionInfo.trans_id, erdData);
}, saveERDData, 500);
useEffect(()=>{
const showSql = ()=>{
eventBus.fireEvent(ERD_EVENTS.SHOW_SQL, checkedMenuItems['sql_with_drop']);
@ -333,6 +344,7 @@ MainToolBar.propTypes = {
textColor: PropTypes.string,
notation: PropTypes.string,
onNotationChange: PropTypes.func,
connectionInfo: PropTypes.object,
};
const ColorButton = withColorPicker(PgIconButton);

View File

@ -10,6 +10,7 @@ import json
import os
import select
import struct
import config
import re
import subprocess
@ -23,12 +24,11 @@ from flask_security import current_user
from pgadmin.user_login_check import pga_login_required
from pgadmin.browser.utils import underscore_unescape, underscore_escape
from pgadmin.utils import PgAdminModule
from pgadmin.utils.constants import MIMETYPE_APP_JS
from pgadmin.utils.driver import get_driver
from ... import socketio as sio
from pgadmin.utils import get_complete_file_path
from pgadmin.authenticate import socket_login_required
from pgadmin.model import Server
if _platform == 'win32':
# Check Windows platform support for WinPty api, Disable psql
@ -81,32 +81,37 @@ def panel(trans_id):
Return panel template for PSQL tools.
:param trans_id:
"""
params = {
'trans_id': trans_id,
'title': request.form['title']
}
if 'sid_soid_mapping' not in app.config:
app.config['sid_soid_mapping'] = dict()
params = {'trans_id': trans_id,
'is_enable':config.ENABLE_PSQL,
'platform': _platform
}
if request.args:
params.update({k: v for k, v in request.args.items()})
if request.form:
for key, val in request.form.items():
params[key] = val
data = _get_database_role(params['sid'], params['did'])
params['title'] = underscore_escape(params['title'])
if 'user' in params:
params['user'] = underscore_escape(params['user'])
params = {
'sid': params['sid'],
'db': underscore_escape(data['db_name']),
'server_type': params['server_type'],
'is_enable': config.ENABLE_PSQL,
'title': underscore_escape(params['title']),
'theme': params['theme'],
'o_db_name': underscore_escape(data['db_name']),
'role': underscore_escape(data['role']),
'platform': _platform
}
if 'sid_soid_mapping' not in app.config:
app.config['sid_soid_mapping'] = dict()
set_env_variables(is_win=_platform == 'win32')
return render_template("psql/index.html",
params=json.dumps(params))
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'])
set_env_variables(is_win=_platform == 'win32')
return render_template("psql/index.html",
params=json.dumps(params))
else:
params['error'] = 'Server did not find.'
return render_template(
"psql/index.html",
params=json.dumps(params))
def set_env_variables(is_win=False):
@ -618,7 +623,8 @@ def _get_database_role(sid, did):
db_name = conn.db
role = manager.role if manager.role else None
return {'db_name': db_name, 'role': role}
except Exception:
except Exception as e:
print(str(e))
return None

View File

@ -10,7 +10,7 @@
import { getRandomInt, hasBinariesConfiguration } from 'sources/utils';
import { retrieveAncestorOfTypeServer } from 'sources/tree/tree_utils';
import { generateTitle } from 'tools/sqleditor/static/js/sqleditor_title';
import { AllPermissionTypes, BROWSER_PANELS } from '../../../../browser/static/js/constants';
import { AllPermissionTypes, BROWSER_PANELS, WORKSPACES } from '../../../../browser/static/js/constants';
import usePreferences,{ listenPreferenceBroadcast } from '../../../../preferences/static/js/store';
import 'pgadmin.browser.keyboard';
import pgWindow from 'sources/window';
@ -25,7 +25,8 @@ import Theme from '../../../../static/js/Theme';
import { NotifierProvider } from '../../../../static/js/helpers/Notifier';
import ModalProvider from '../../../../static/js/helpers/ModalProvider';
import * as csrfToken from 'sources/csrf';
import { ApplicationStateProvider } from '../../../../settings/static/ApplicationStateProvider';
import ToolErrorView from '../../../../static/js/ToolErrorView';
import React from 'react';
import ReactDOM from 'react-dom/client';
@ -103,35 +104,61 @@ export default class Psql {
}
}
openPsqlTool(data, treeIdentifier) {
openPsqlTool(_data, treeIdentifier, connectionInfo=null) {
let parentData = null;
let panelTitle = '';
if (connectionInfo){
parentData = {
server_group: {
_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
},
schema: {
_id: connectionInfo.scid || null,
},
table: {
_id: connectionInfo.tid || null,
}
};
const serverInformation = retrieveAncestorOfTypeServer(pgBrowser, treeIdentifier, gettext('PSQL Error'));
if (!hasBinariesConfiguration(pgBrowser, serverInformation)) {
return;
}
}else{
const serverInformation = retrieveAncestorOfTypeServer(pgBrowser, treeIdentifier, gettext('PSQL Error'));
if (!hasBinariesConfiguration(pgBrowser, serverInformation)) {
return;
}
const node = pgBrowser.tree.findNodeByDomElement(treeIdentifier);
if (node === undefined || !node.getData()) {
pgAdmin.Browser.notifier.alert(
gettext('PSQL Error'),
gettext('No object selected.')
);
return;
}
const node = pgBrowser.tree.findNodeByDomElement(treeIdentifier);
if (node === undefined || !node.getData()) {
pgAdmin.Browser.notifier.alert(
gettext('PSQL Error'),
gettext('No object selected.')
);
return;
}
const parentData = pgBrowser.tree.getTreeNodeHierarchy(treeIdentifier);
parentData = pgBrowser.tree.getTreeNodeHierarchy(treeIdentifier);
if(_.isUndefined(parentData.server)) {
pgAdmin.Browser.notifier.alert(
gettext('PSQL Error'),
gettext('Please select a server/database object.')
);
return;
}
if(_.isUndefined(parentData.server)) {
pgAdmin.Browser.notifier.alert(
gettext('PSQL Error'),
gettext('Please select a server/database object.')
);
return;
}
const transId = getRandomInt(1, 9999999);
let panelTitle = '';
// Set psql tab title as per prefrences setting.
let title_data = {
'database': parentData.database ? _.unescape(parentData.database.label) : 'postgres' ,
@ -139,6 +166,7 @@ export default class Psql {
'server': parentData.server.label,
'type': 'psql_tool',
};
let tab_title_placeholder = usePreferences.getState().getPreferencesForModule('browser').psql_tab_title_placeholder;
panelTitle = generateTitle(tab_title_placeholder, title_data);
@ -150,7 +178,7 @@ export default class Psql {
'pgadmin:tool:show',
`${BROWSER_PANELS.PSQL_TOOL}_${transId}`,
panelUrl,
{title: panelTitle, db: db_label},
{title: panelTitle, db: db_label, server_name: parentData.server.label, 'user': parentData.server.user.name },
{title: panelTitle, icon: 'pg-font-icon icon-terminal', manualClose: false, renamable: true},
Boolean(open_new_tab?.includes('psql_tool'))
);
@ -180,23 +208,35 @@ export default class Psql {
return [openUrl, pData.database._label];
}
async loadComponent(container, params) {
let panelDocker = pgWindow.pgAdmin.Browser.docker.psql_workspace;
if (pgWindow.pgAdmin.Browser.docker.currentWorkspace == WORKSPACES.DEFAULT) {
panelDocker = pgWindow.pgAdmin.Browser.docker.default_workspace;
}
pgAdmin.Browser.keyboardNavigation.init();
await listenPreferenceBroadcast();
const root = ReactDOM.createRoot(container);
root.render(
<Theme>
<PgAdminProvider value={pgAdmin}>
<ModalProvider>
<NotifierProvider pgAdmin={pgAdmin} pgWindow={pgWindow} />
<PsqlComponent params={params} pgAdmin={pgAdmin} />
</ModalProvider>
<ApplicationStateProvider>
<ModalProvider>
<NotifierProvider pgAdmin={pgAdmin} pgWindow={pgWindow} />
{ params.error ?
<ToolErrorView
error={params.error}
panelId={`${BROWSER_PANELS.PSQL_TOOL}_${params.trans_id}`}
panelDocker={panelDocker}
/> :
<PsqlComponent
params={params}
pgAdmin={pgAdmin} />
}
</ModalProvider>
</ApplicationStateProvider>
</PgAdminProvider>
</Theme>
);
}
}

View File

@ -7,7 +7,7 @@
//
//////////////////////////////////////////////////////////////
import React, { useEffect } from 'react';
import React, { useEffect, useCallback, useRef } from 'react';
import { Box, styled, useTheme } from '@mui/material';
import url_for from 'sources/url_for';
import PropTypes from 'prop-types';
@ -20,7 +20,7 @@ import { io } from 'socketio';
import { copyToClipboard } from '../../../../../static/js/clipboard';
import 'pgadmin.browser.keyboard';
import gettext from 'sources/gettext';
import { useApplicationState } from '../../../../../settings/static/ApplicationStateProvider';
const Root = styled(Box)(()=>({
width: '100%',
@ -129,14 +129,8 @@ function psql_terminal_io(term, socket, platform, pgAdmin) {
function psql_Addon(term) {
const fitAddon = new FitAddon();
term.loadAddon(fitAddon);
term.loadAddon(new WebLinksAddon());
term.loadAddon(new SearchAddon());
fitAddon.fit();
term.resize(15, 50);
fitAddon.fit();
return fitAddon;
}
@ -152,32 +146,29 @@ export default function PsqlComponent({ params, pgAdmin }) {
const theme = useTheme();
const termRef = React.useRef(null);
const containerRef = React.useRef(null);
const fitAddonRef = useRef(null);
const {saveToolData, isSaveToolDataEnabled} = useApplicationState();
const initializePsqlTool = (params)=>{
const initializePsqlTool = useCallback((params)=>{
const term = new Terminal({
cursorBlink: true,
scrollback: 5000,
});
/* Addon for fitAddon, webLinkAddon, SearchAddon */
const fitAddon = psql_Addon(term);
fitAddonRef.current = psql_Addon(term);
/* Open terminal */
term.open(containerRef.current);
/* Socket */
const socket = psql_socket();
psql_socket_io(socket, params.is_enable, params.sid, params.db, params.server_type, fitAddon, term, params.role);
psql_socket_io(socket, params.is_enable, params.sid, params.db, params.server_type, fitAddonRef.current, term, params.role);
psql_terminal_io(term, socket, params.platform, pgAdmin);
/* Set terminal size */
setTimeout(function(){
socket.emit('resize', {'cols': term.cols, 'rows': term.rows});
}, 1000);
return [term, socket];
};
}, [params, pgAdmin]);;
const setTheme = ()=>{
const setTheme = useCallback(()=>{
if(termRef.current) {
termRef.current.options.theme = {
background: theme.palette.background.default,
@ -187,30 +178,43 @@ export default function PsqlComponent({ params, pgAdmin }) {
selectionBackground: `${theme.otherVars.editor.selectionBg}`,
};
}
};
}, [theme]);
useEffect(()=>{
const [term, socket] = initializePsqlTool(params);
termRef.current = term;
setTheme();
termRef.current.focus();
const observer = new ResizeObserver((entries) => {
for (let entry of entries) {
if (entry.contentRect.width > 0 && entry.contentRect.height > 0) {
fitAddonRef.current?.fit();
socket.emit('resize', { cols: term.cols, rows: term.rows});
term.focus();
observer.disconnect(); // Only do this once
}
}
});
termRef.current.focus();
if (containerRef.current) {
observer.observe(containerRef.current);
}
if(isSaveToolDataEnabled('psql')){
saveToolData('psql', params, params.trans_id, null);
}
return () => {
term.dispose();
socket.disconnect();
observer.disconnect();
};
}, []);
useEffect(()=>{
setTheme();
},[theme]);
return (
<Root ref={containerRef}>
</Root>
@ -224,7 +228,8 @@ PsqlComponent.propTypes = {
db: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
server_type: PropTypes.string,
role: PropTypes.string,
platform: PropTypes.string
platform: PropTypes.string,
trans_id: PropTypes.number
}),
pgAdmin: PropTypes.object.isRequired,
};

View File

@ -10,10 +10,9 @@
"""A blueprint module implementing the schema_diff frame."""
import json
import pickle
import secrets
import copy
from flask import Response, session, url_for, request
from flask import Response, session, request
from flask import render_template, current_app as app
from flask_security import current_user, permissions_required
from pgadmin.user_login_check import pga_login_required
@ -26,7 +25,7 @@ from pgadmin.tools.schema_diff.node_registry import SchemaDiffRegistry
from pgadmin.tools.schema_diff.model import SchemaDiffModel
from config import PG_DEFAULT_DRIVER
from pgadmin.utils.driver import get_driver
from pgadmin.utils.constants import PREF_LABEL_DISPLAY, MIMETYPE_APP_JS,\
from pgadmin.utils.constants import PREF_LABEL_DISPLAY, \
ERROR_MSG_TRANS_ID_NOT_FOUND
from sqlalchemy import or_
from pgadmin.authenticate import socket_login_required
@ -121,7 +120,7 @@ def index():
@blueprint.route(
'/panel/<int:trans_id>/<path:editor_title>',
methods=["GET"],
methods=["POST"],
endpoint='panel'
)
@permissions_required(AllPermissionTypes.tools_schema_diff)
@ -133,6 +132,7 @@ def panel(trans_id, editor_title):
Args:
editor_title: Title of the editor
"""
params = {}
# If title has slash(es) in it then replace it
if request.args and request.args['fslashes'] != '':
try:
@ -142,12 +142,18 @@ def panel(trans_id, editor_title):
editor_title = editor_title[:idx] + '/' + editor_title[idx:]
except IndexError as e:
app.logger.exception(e)
if request.args:
params = {k: v for k, v in request.args.items()}
if request.form:
for key, val in request.form.items():
params[key] = val
return render_template(
"schema_diff/index.html",
_=gettext,
trans_id=trans_id,
editor_title=editor_title,
params=json.dumps(params)
)

View File

@ -24,6 +24,7 @@ import { NotifierProvider } from '../../../../static/js/helpers/Notifier';
import usePreferences, { listenPreferenceBroadcast } from '../../../../preferences/static/js/store';
import pgAdmin from 'sources/pgadmin';
import { PgAdminProvider } from '../../../../static/js/PgAdminProvider';
import { ApplicationStateProvider } from '../../../../settings/static/ApplicationStateProvider';
export default class SchemaDiff {
static instance;
@ -61,7 +62,7 @@ export default class SchemaDiff {
}]);
}
launchSchemaDiff() {
launchSchemaDiff(toolDataId=null) {
let panelTitle = SchemaDiff.panelTitleCount > 1 ? gettext('Schema Diff - %s', SchemaDiff.panelTitleCount) : gettext('Schema Diff');
SchemaDiff.panelTitleCount++;
const trans_id = commonUtils.getRandomInt(1, 9999999);
@ -79,24 +80,26 @@ export default class SchemaDiff {
'pgadmin:tool:show',
`${BROWSER_PANELS.SCHEMA_DIFF_TOOL}_${trans_id}`,
baseUrl,
null,
{toolDataId: toolDataId},
{title: panelTitle, icon: 'pg-font-icon icon-compare', manualClose: false, renamable: true},
Boolean(openInNewTab?.includes('schema_diff'))
);
return true;
}
async load(container, trans_id) {
async load(container, trans_id, params) {
pgAdmin.Browser.keyboardNavigation.init();
await listenPreferenceBroadcast();
const root = ReactDOM.createRoot(container);
root.render(
<Theme>
<PgAdminProvider value={pgAdmin}>
<ModalProvider>
<NotifierProvider pgAdmin={pgAdmin} pgWindow={pgWindow} />
<SchemaDiffComponent params={{ transId: trans_id, pgAdmin: pgWindow.pgAdmin }}></SchemaDiffComponent>
</ModalProvider>
<ApplicationStateProvider>
<ModalProvider>
<NotifierProvider pgAdmin={pgAdmin} pgWindow={pgWindow} />
<SchemaDiffComponent params={{ transId: trans_id, pgAdmin: pgWindow.pgAdmin, params:params }}></SchemaDiffComponent>
</ModalProvider>
</ApplicationStateProvider>
</PgAdminProvider>
</Theme>
);

View File

@ -7,16 +7,12 @@
//
//////////////////////////////////////////////////////////////
import PropTypes from 'prop-types';
import React, { useContext, useState, useEffect } from 'react';
import { Box, Grid, Typography } from '@mui/material';
import { InputSelect } from '../../../../../static/js/components/FormComponents';
import { SchemaDiffEventsContext } from './SchemaDiffComponent';
import { SCHEMA_DIFF_EVENT } from '../SchemaDiffConstants';
export function InputComponent({ label, serverList, databaseList, schemaList, diff_type, selectedSid = null, selectedDid=null, selectedScid=null, onServerSchemaChange }) {
const [selectedServer, setSelectedServer] = useState(selectedSid);
const [selectedDatabase, setSelectedDatabase] = useState(selectedDid);
@ -25,10 +21,18 @@ export function InputComponent({ label, serverList, databaseList, schemaList, di
const [disableDBSelection, setDisableDBSelection] = useState(selectedSid == null);
const [disableSchemaSelection, setDisableSchemaSelection] = useState(selectedDid == null);
useEffect(() => {
setSelectedDatabase(selectedDid);
if (selectedDid) setDisableSchemaSelection(false);
}, [selectedSid, selectedDid, selectedScid]);
useEffect(()=>{
changeServer(selectedSid);
},[selectedSid]);
useEffect(()=>{
changeDatabase(selectedDid);
},[selectedDid]);
useEffect(()=>{
changeSchema(selectedScid);
},[selectedScid]);
const changeServer = (selectedOption) => {
setDisableDBSelection(false);
@ -43,6 +47,7 @@ export function InputComponent({ label, serverList, databaseList, schemaList, di
eventBus.fireEvent(SCHEMA_DIFF_EVENT.TRIGGER_SELECT_SERVER, { selectedOption, diff_type, serverList });
};
const changeDatabase = (selectedDB) => {
setSelectedDatabase(selectedDB);
setDisableSchemaSelection(false);

View File

@ -32,7 +32,8 @@ import { ResultGridComponent } from './ResultGridComponent';
import { openSocket, socketApiGet } from '../../../../../static/js/socket_instance';
import { parseApiError } from '../../../../../static/js/api_instance';
import { usePgAdmin } from '../../../../../static/js/PgAdminProvider';
import { useApplicationState } from '../../../../../settings/static/ApplicationStateProvider';
import { getToolData } from '../../../../../settings/static/ApplicationStateProvider';
function generateFinalScript(script_array, scriptHeader, script_body) {
_.each(Object.keys(script_array).reverse(), function (s) {
@ -117,6 +118,8 @@ export function SchemaDiffCompare({ params }) {
const [isInit, setIsInit] = useState(true);
const pgAdmin = usePgAdmin();
const {saveToolData, isSaveToolDataEnabled} = useApplicationState();
const [oldSchemaDiffData, setOldSchemaDiffData] = useState([]);
useEffect(() => {
schemaDiffToolContext.api.get(url_for('schema_diff.servers')).then((res) => {
@ -137,6 +140,23 @@ export function SchemaDiffCompare({ params }) {
});
}, []);
useEffect(()=>{
let oldSchemaDiffData1 = getToolData(params.params?.toolDataId);
setOldSchemaDiffData(oldSchemaDiffData1);
},[]);
useEffect(()=>{
if(oldSchemaDiffData){
_.each(oldSchemaDiffData,(d)=>{
if(d.diff_type == TYPE.SOURCE){
setSelectedSourceSid(d.selectedSourceSid);
}else{
setSelectedTargetSid(d.selectedTargetSid);
}
});
}
},[sourceGroupServerList]);
useEffect(() => {
// Register all eventes for debugger.
eventBus.registerListener(
@ -145,7 +165,6 @@ export function SchemaDiffCompare({ params }) {
eventBus.registerListener(
SCHEMA_DIFF_EVENT.TRIGGER_SELECT_DATABASE, triggerSelectDatabase);
eventBus.registerListener(
SCHEMA_DIFF_EVENT.TRIGGER_SELECT_SCHEMA, triggerSelectSchema);
@ -159,7 +178,7 @@ export function SchemaDiffCompare({ params }) {
SCHEMA_DIFF_EVENT.TRIGGER_GENERATE_SCRIPT, triggerGenerateScript);
}, []);
function checkAndSetSourceData(diff_type, selectedOption) {
if(selectedOption == null) {
setSelectedRowIds([]);
@ -264,6 +283,15 @@ export function SchemaDiffCompare({ params }) {
pgAdmin.Browser.notifier.alert(gettext('Selection Error'),
gettext('Please select the different source and target.'));
} else {
if(isSaveToolDataEnabled('schema_diff')){
let toolData = [
{ diff_type: TYPE.SOURCE, selectedSourceSid: sourceData.sid, selectedSourceDid:sourceData.did, selectedSourceScid: sourceData.scid},
{ diff_type: TYPE.TARGET, selectedTargetSid:targetData.sid, selectedTargetDid:targetData.did, selectedTargetScid:targetData.scid },
];
saveToolData('schema_diff', null, params.transId, toolData);
}
setLoaderText('Comparing objects... (this may take a few minutes)...');
let url_params = {
'trans_id': params.transId,
@ -626,7 +654,6 @@ export function SchemaDiffCompare({ params }) {
url_for('schema_diff.databases', { 'sid': sid })
).then((res) => {
res.data.data.map((opt) => {
if (opt.is_maintenance_db) {
if (diff_type == TYPE.SOURCE) {
setSelectedSourceDid(opt.value);
@ -641,10 +668,21 @@ export function SchemaDiffCompare({ params }) {
} else {
setTargetDatabaseList(res.data.data);
}
});
}
useEffect(()=>{
if(oldSchemaDiffData){
_.each(oldSchemaDiffData,(d)=>{
if(d.diff_type == TYPE.SOURCE){
setSelectedSourceDid(d.selectedSourceDid);
}else{
setSelectedTargetDid(d.selectedTargetDid);
}
});
}
},[targetDatabaseList, sourceDatabaseList]);
function getSchemaList(sid, did, diff_type) {
schemaDiffToolContext.api.get(
url_for('schema_diff.schemas', { 'sid': sid, 'did': did })
@ -654,10 +692,21 @@ export function SchemaDiffCompare({ params }) {
} else {
setTargetSchemaList(res.data.data);
}
});
}
useEffect(()=>{
if(oldSchemaDiffData){
_.each(oldSchemaDiffData,(d)=>{
if(d.diff_type == TYPE.SOURCE){
setSelectedSourceScid(d.selectedSourceScid);
}else{
setSelectedTargetScid(d.selectedTargetScid);
}
});
}
},[targetSchemaList, sourceSchemaList]);
function showConnectServer(result, sid, diff_type, serverList) {
schemaDiffToolContext.modal.showModal(gettext('Connect to server'), (closeModal) => {
return (

View File

@ -25,7 +25,6 @@ import getApiInstance, { callFetch } from '../../../../../static/js/api_instance
import { useModal } from '../../../../../static/js/helpers/ModalProvider';
import usePreferences from '../../../../../preferences/static/js/store';
export const SchemaDiffEventsContext = createContext();
export const SchemaDiffContext = createContext();

View File

@ -5,7 +5,7 @@ try {
['sources/generated/browser_nodes', 'sources/generated/schema_diff'],
function() {
var pgSchemaDiff = window.pgAdmin.Tools.SchemaDiff;
pgSchemaDiff.load(document.getElementById('schema-diff-main-container'),{{trans_id}});
pgSchemaDiff.load(document.getElementById('schema-diff-main-container'),{{trans_id}},{{ params|safe }});
},
function() {
console.log(arguments);

View File

@ -42,11 +42,11 @@ from pgadmin.tools.sqleditor.utils.update_session_grid_transaction import \
from pgadmin.utils import PgAdminModule
from pgadmin.utils import get_storage_directory
from pgadmin.utils.ajax import make_json_response, bad_request, \
success_return, internal_server_error, service_unavailable
success_return, internal_server_error, service_unavailable, gone
from pgadmin.utils.driver import get_driver
from pgadmin.utils.exception import ConnectionLost, SSHTunnelConnectionLost, \
CryptKeyMissing, ObjectGone
from pgadmin.browser.utils import underscore_unescape, underscore_escape
from pgadmin.browser.utils import underscore_escape
from pgadmin.utils.menu import MenuItem
from pgadmin.utils.sqlautocomplete.autocomplete import SQLAutoComplete
from pgadmin.tools.sqleditor.utils.query_tool_preferences import \
@ -146,7 +146,7 @@ class SqlEditorModule(PgAdminModule):
'sqleditor.get_new_connection_user',
'sqleditor._check_server_connection_status',
'sqleditor.get_new_connection_role',
'sqleditor.connect_server',
'sqleditor.connect_server'
]
def on_logout(self):
@ -325,38 +325,47 @@ def panel(trans_id):
params['fgcolor'] = None
s = Server.query.filter_by(id=int(params['sid'])).first()
if s.shared and s.user_id != current_user.id:
# Import here to avoid circular dependency
from pgadmin.browser.server_groups.servers import ServerModule
shared_server = ServerModule.get_shared_server(s, params['sgid'])
s = ServerModule.get_shared_server_properties(s, shared_server)
if s:
if s.shared and s.user_id != current_user.id:
# Import here to avoid circular dependency
from pgadmin.browser.server_groups.servers import ServerModule
shared_server = ServerModule.get_shared_server(s, params['sgid'])
s = ServerModule.get_shared_server_properties(s, shared_server)
if s and s.bgcolor:
# If background is set to white means we do not have to change
# the title background else change it as per user specified
# background
if s.bgcolor != '#ffffff':
params['bgcolor'] = s.bgcolor
params['fgcolor'] = s.fgcolor or 'black'
if s and s.bgcolor:
# If background is set to white means we do not have to change
# the title background else change it as per user specified
# background
if s.bgcolor != '#ffffff':
params['bgcolor'] = s.bgcolor
params['fgcolor'] = s.fgcolor or 'black'
params['server_name'] = underscore_escape(s.name)
if 'user' not in params:
params['user'] = underscore_escape(s.username)
if 'role' not in params and s.role:
params['role'] = underscore_escape(s.role)
params['layout'] = get_setting('SQLEditor/Layout')
params['macros'] = get_user_macros()
params['is_desktop_mode'] = current_app.PGADMIN_RUNTIME
params['title'] = underscore_escape(params['title'])
params['selectedNodeInfo'] = underscore_escape(params['selectedNodeInfo'])
if 'database_name' in params:
params['database_name'] = underscore_escape(params['database_name'])
params['server_name'] = underscore_escape(s.name)
if 'user' not in params:
params['user'] = underscore_escape(s.username)
if 'role' not in params and s.role:
params['role'] = underscore_escape(s.role)
params['layout'] = get_setting('SQLEditor/Layout')
params['macros'] = get_user_macros()
params['is_desktop_mode'] = current_app.PGADMIN_RUNTIME
params['title'] = underscore_escape(params['title'])
params['selectedNodeInfo'] = (
underscore_escape(params['selectedNodeInfo']))
if 'database_name' in params:
params['database_name'] = (
underscore_escape(params['database_name']))
return render_template(
"sqleditor/index.html",
title=underscore_escape(params['title']),
params=json.dumps(params),
)
return render_template(
"sqleditor/index.html",
title=underscore_escape(params['title']),
params=json.dumps(params),
)
else:
params['error'] = 'Server did not find.'
return render_template(
"sqleditor/index.html",
title=None,
params=json.dumps(params))
@blueprint.route(
@ -655,6 +664,7 @@ def close(trans_id):
# session variable.
grid_data.pop(str(trans_id), None)
session['gridData'] = grid_data
except Exception as e:
current_app.logger.error(e)
return internal_server_error(errormsg=str(e))

View File

@ -27,6 +27,8 @@ import { AllPermissionTypes, BROWSER_PANELS, WORKSPACES } from '../../../../brow
import { NotifierProvider } from '../../../../static/js/helpers/Notifier';
import usePreferences, { listenPreferenceBroadcast } from '../../../../preferences/static/js/store';
import { PgAdminProvider } from '../../../../static/js/PgAdminProvider';
import { ApplicationStateProvider } from '../../../../settings/static/ApplicationStateProvider';
import ToolErrorView from '../../../../static/js/ToolErrorView';
export default class SQLEditor {
static instance;
@ -218,7 +220,7 @@ export default class SQLEditor {
let browser_preferences = usePreferences.getState().getPreferencesForModule('browser');
let open_new_tab = browser_preferences.new_browser_tab_open;
const [icon, tooltip] = panelTitleFunc.getQueryToolIcon(panel_title, is_query_tool);
let selectedNodeInfo = pgAdmin.Browser.tree.getTreeNodeHierarchy(
let selectedNodeInfo = pgAdmin.Browser.tree?.getTreeNodeHierarchy(
pgAdmin.Browser.tree.selected()
);
@ -246,11 +248,24 @@ export default class SQLEditor {
root.render(
<Theme>
<PgAdminProvider value={pgAdmin}>
<ModalProvider>
<NotifierProvider pgAdmin={pgAdmin} pgWindow={pgWindow} />
<QueryToolComponent params={params} pgWindow={pgWindow} pgAdmin={pgAdmin} qtPanelDocker={panelDocker}
qtPanelId={`${BROWSER_PANELS.QUERY_TOOL}_${params.trans_id}`} selectedNodeInfo={selectedNodeInfo}/>
</ModalProvider>
<ApplicationStateProvider>
<ModalProvider>
<NotifierProvider pgAdmin={pgAdmin} pgWindow={pgWindow} />
{ params.error ?
<ToolErrorView
error={params.error}
panelId={`${BROWSER_PANELS.QUERY_TOOL}_${params.trans_id}`}
panelDocker={panelDocker}
/> :
<QueryToolComponent params={params}
pgWindow={pgWindow}
pgAdmin={pgAdmin}
qtPanelDocker={panelDocker}
qtPanelId={`${BROWSER_PANELS.QUERY_TOOL}_${params.trans_id}`}
selectedNodeInfo={selectedNodeInfo}
/>}
</ModalProvider>
</ApplicationStateProvider>
</PgAdminProvider>
</Theme>
);

View File

@ -37,6 +37,7 @@ 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';
export const QueryToolContext = React.createContext();
export const QueryToolConnectionContext = React.createContext();
@ -215,6 +216,7 @@ export default function QueryToolComponent({params, pgWindow, pgAdmin, selectedN
const docker = useRef(null);
const api = useMemo(()=>getApiInstance(), []);
const modal = useModal();
const {isSaveToolDataEnabled} = useApplicationState();
/* Connection status poller */
let pollTime = qtState.preferences.sqleditor.connection_status_fetch_time > 0
@ -332,17 +334,35 @@ export default function QueryToolComponent({params, pgWindow, pgAdmin, selectedN
setQtStatePartial({ editor_disabled: true });
});
} else if (qtState.params.sql_id) {
let sqlValue = localStorage.getItem(qtState.params.sql_id);
localStorage.removeItem(qtState.params.sql_id);
if (sqlValue) {
eventBus.current.fireEvent(QUERY_TOOL_EVENTS.EDITOR_SET_SQL, sqlValue);
}
setQtStatePartial({ editor_disabled: false });
populateEditorData();
} else {
setQtStatePartial({ editor_disabled: false });
}
};
const populateEditorData = () =>{
let sqlId = qtState.params.sql_id,
loadSqlFromLocalStorage = true;
if(qtState.params.open_file_name){
if(qtState.params.file_deleted == 'false' && qtState.params.is_editor_dirty == 'false'){
// call load file from disk as no fil changes
eventBus.current.fireEvent(QUERY_TOOL_EVENTS.LOAD_FILE, qtState.params.open_file_name, qtState.params?.storage);
}else{
if(qtState.params.file_deleted != 'true'){
if(qtState.params.external_file_changes == 'true'){
loadSqlFromLocalStorage = false;
eventBus.current.fireEvent(QUERY_TOOL_EVENTS.WARN_RELOAD_FILE, qtState.params.open_file_name, sqlId);
}else{
eventBus.current.fireEvent(QUERY_TOOL_EVENTS.LOAD_FILE_DONE, qtState.params.open_file_name, true);
}
}
}
}
if(loadSqlFromLocalStorage) eventBus.current.fireEvent(QUERY_TOOL_EVENTS.LOAD_SQL_FROM_LOCAL_STORAGE, sqlId);
setQtStatePartial({ editor_disabled: false });
};
const initializeQueryTool = (password, explainObject=null, macroSQL='', executeCursor=false, reexecute=false)=>{
let selectedConn = _.find(qtState.connection_list, (c)=>c.is_selected);
let baseUrl = '';
@ -499,7 +519,7 @@ export default function QueryToolComponent({params, pgWindow, pgAdmin, selectedN
'trans_id': qtState.params.trans_id,
}), {
keepalive: true,
method: 'DELETE',
method: 'DELETE'
}
)
.then(()=>{/* Success */})
@ -573,10 +593,12 @@ export default function QueryToolComponent({params, pgWindow, pgAdmin, selectedN
const fileDone = (fileName, success=true)=>{
if(success) {
setQtStatePartial({
current_file: fileName,
current_file: fileName
});
isDirtyRef.current = false;
setPanelTitle(qtPanelDocker, qtPanelId, fileName, {...qtState, current_file: fileName}, isDirtyRef.current);
if(isSaveToolDataEnabled('sqleditor'))eventBus.current.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_SAVE_QUERY_TOOL_DATA);
}
eventBus.current.fireEvent(QUERY_TOOL_EVENTS.EDITOR_LAST_FOCUS);
};
@ -898,6 +920,8 @@ export default function QueryToolComponent({params, pgWindow, pgAdmin, selectedN
mainContainerRef: containerRef,
editor_disabled: qtState.editor_disabled,
eol: qtState.eol,
connection_list: qtState.connection_list,
current_file: qtState.current_file,
toggleQueryTool: () => setQtStatePartial((prev)=>{
return {
...prev,
@ -928,7 +952,7 @@ export default function QueryToolComponent({params, pgWindow, pgAdmin, selectedN
};
});
},
}), [qtState.params, qtState.preferences, containerRef.current, qtState.editor_disabled, qtState.eol]);
}), [qtState.params, qtState.preferences, containerRef.current, qtState.editor_disabled, qtState.eol, qtState.current_file]);
const queryToolConnContextValue = React.useMemo(()=>({
connected: qtState.connected,

View File

@ -29,6 +29,7 @@ export const QUERY_TOOL_EVENTS = {
TRIGGER_FORMAT_SQL: 'TRIGGER_FORMAT_SQL',
TRIGGER_GRAPH_VISUALISER: 'TRIGGER_GRAPH_VISUALISER',
TRIGGER_SELECT_ALL: 'TRIGGER_SELECT_ALL',
TRIGGER_SAVE_QUERY_TOOL_DATA: 'TRIGGER_SAVE_QUERY_TOOL_DATA',
COPY_DATA: 'COPY_DATA',
SET_LIMIT_VALUE: 'SET_LIMIT_VALUE',
@ -48,6 +49,8 @@ export const QUERY_TOOL_EVENTS = {
LOAD_FILE_DONE: 'LOAD_FILE_DONE',
SAVE_FILE: 'SAVE_FILE',
SAVE_FILE_DONE: 'SAVE_FILE_DONE',
SAVE_QUERY_TOOL_DATA: 'SAVE_QUERY_TOOL_DATA',
LOAD_SQL_FROM_LOCAL_STORAGE: 'LOAD_SQL_FROM_LOCAL_STORAGE',
QUERY_CHANGED: 'QUERY_CHANGED',
API_ERROR: 'API_ERROR',
TASK_START: 'TASK_START',
@ -73,6 +76,7 @@ export const QUERY_TOOL_EVENTS = {
WARN_SAVE_DATA_CLOSE: 'WARN_SAVE_DATA_CLOSE',
WARN_SAVE_TEXT_CLOSE: 'WARN_SAVE_TEXT_CLOSE',
WARN_RELOAD_FILE: 'WARN_RELOAD_FILE',
WARN_TXN_CLOSE: 'WARN_TXN_CLOSE',
EXECUTE_CURSOR_WARNING: 'EXECUTE_CURSOR_WARNING',

View File

@ -6,7 +6,7 @@
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import React, {useContext, useCallback, useEffect, useMemo } from 'react';
import React, {useContext, useCallback, useEffect, useMemo, useState } from 'react';
import { format } from 'sql-formatter';
import { QueryToolContext, QueryToolEventsContext } from '../QueryToolComponent';
import CodeMirror from '../../../../../../static/js/components/ReactCodeMirror';
@ -25,6 +25,9 @@ import usePreferences from '../../../../../../preferences/static/js/store';
import { getTitle } from '../../sqleditor_title';
import PropTypes from 'prop-types';
import { MODAL_DIALOGS } from '../QueryToolConstants';
import { useApplicationState } from '../../../../../../settings/static/ApplicationStateProvider';
import { useDelayDebounce } from '../../../../../../static/js/custom_hooks';
import { getToolData } from '../../../../../../settings/static/ApplicationStateProvider';
async function registerAutocomplete(editor, api, transId) {
@ -64,9 +67,11 @@ export default function Query({onTextSelect, setQtStatePartial}) {
const layoutDocker = useContext(LayoutDockerContext);
const lastCursorPos = React.useRef();
const pgAdmin = usePgAdmin();
const {saveToolData, isSaveToolDataEnabled} = useApplicationState();
const preferencesStore = usePreferences();
const queryToolPref = queryToolCtx.preferences.sqleditor;
const modalId = MODAL_DIALOGS.QT_CONFIRMATIONS;
const highlightError = (cmObj, {errormsg: result, data}, executeCursor)=>{
let errorLineNo = 0,
startMarker = 0,
@ -136,7 +141,6 @@ export default function Query({onTextSelect, setQtStatePartial}) {
cmObj.setCursor(errorLineNo, endMarker);
}
};
const triggerExecution = (explainObject, macroSQL, executeCursor=false)=>{
if(queryToolCtx.params.is_query_tool) {
let external = null;
@ -160,12 +164,27 @@ export default function Query({onTextSelect, setQtStatePartial}) {
}
};
const warnReloadFile = (fileName, sqlId, storage=null)=>{
queryToolCtx.modal.confirm(
gettext('Reload file?'),
gettext('The file has been modified by another program. Do you want to reload it and loose changes made in pgadmin?'),
function() {
eventBus.fireEvent(QUERY_TOOL_EVENTS.LOAD_FILE, fileName);
},
function() {
eventBus.fireEvent(QUERY_TOOL_EVENTS.LOAD_SQL_FROM_LOCAL_STORAGE, sqlId);
eventBus.fireEvent(QUERY_TOOL_EVENTS.LOAD_FILE_DONE, fileName, true, storage);
}
);
};
useEffect(()=>{
layoutDocker.eventBus.registerListener(LAYOUT_EVENTS.ACTIVE, (currentTabId)=>{
currentTabId == PANELS.QUERY && editor.current.focus();
});
eventBus.registerListener(QUERY_TOOL_EVENTS.TRIGGER_EXECUTION, triggerExecution);
eventBus.registerListener(QUERY_TOOL_EVENTS.EXECUTE_CURSOR_WARNING, checkUnderlineQueryCursorWarning);
eventBus.registerListener(QUERY_TOOL_EVENTS.HIGHLIGHT_ERROR, (result, executeCursor)=>{
@ -189,7 +208,7 @@ export default function Query({onTextSelect, setQtStatePartial}) {
editor.current.setValue(res.data);
//Check the file content for Trojan Source
checkTrojanSource(res.data);
eventBus.fireEvent(QUERY_TOOL_EVENTS.LOAD_FILE_DONE, fileName, true);
eventBus.fireEvent(QUERY_TOOL_EVENTS.LOAD_FILE_DONE, fileName, true, storage);
// Detect line separator from content and editor's EOL.
const lineSep = editor.current?.detectEOL(res.data);
// Update the EOL if it differs from the current editor EOL
@ -197,7 +216,7 @@ export default function Query({onTextSelect, setQtStatePartial}) {
// Mark the editor content as clean
editor.current?.markClean();
}).catch((err)=>{
eventBus.fireEvent(QUERY_TOOL_EVENTS.LOAD_FILE_DONE, null, false);
eventBus.fireEvent(QUERY_TOOL_EVENTS.LOAD_FILE_DONE, null, false, storage);
pgAdmin.Browser.notifier.error(parseApiError(err));
});
});
@ -233,6 +252,7 @@ export default function Query({onTextSelect, setQtStatePartial}) {
editor.current?.execCommand(cmd);
}
});
eventBus.registerListener(QUERY_TOOL_EVENTS.COPY_TO_EDITOR, (text)=>{
editor.current?.setValue(text);
eventBus.fireEvent(QUERY_TOOL_EVENTS.FOCUS_PANEL, PANELS.QUERY);
@ -241,6 +261,7 @@ export default function Query({onTextSelect, setQtStatePartial}) {
editor.current?.setCursor(editor.current.lineCount(), 0);
}, 250);
});
eventBus.registerListener(QUERY_TOOL_EVENTS.EDITOR_FIND_REPLACE, (replace=false)=>{
editor.current?.focus();
let key = {
@ -254,6 +275,7 @@ export default function Query({onTextSelect, setQtStatePartial}) {
}
editor.current?.fireDOMEvent(new KeyboardEvent('keydown', key));
});
eventBus.registerListener(QUERY_TOOL_EVENTS.EDITOR_SET_SQL, (value, focus=true)=>{
focus && editor.current?.focus();
editor.current?.setValue(value, !queryToolCtx.params.is_query_tool);
@ -261,6 +283,7 @@ export default function Query({onTextSelect, setQtStatePartial}) {
eventBus.registerListener(QUERY_TOOL_EVENTS.TRIGGER_QUERY_CHANGE, ()=>{
change();
});
eventBus.registerListener(QUERY_TOOL_EVENTS.TRIGGER_FORMAT_SQL, ()=>{
let selection = true, sql = editor.current?.getSelection();
let sqlEditorPref = preferencesStore.getPreferencesForModule('sqleditor');
@ -297,7 +320,7 @@ export default function Query({onTextSelect, setQtStatePartial}) {
eventBus.registerListener(QUERY_TOOL_EVENTS.CHANGE_EOL, (lineSep)=>{
// Set the new EOL character in the editor.
editor.current?.setEOL(lineSep);
eventBus.fireEvent(QUERY_TOOL_EVENTS.QUERY_CHANGED, editor.current?.isDirty());
eventBus.fireEvent(QUERY_TOOL_EVENTS.QUERY_CHANGED, editor.current?.isDirty());
});
eventBus.registerListener(QUERY_TOOL_EVENTS.EDITOR_TOGGLE_CASE, ()=>{
@ -317,10 +340,24 @@ export default function Query({onTextSelect, setQtStatePartial}) {
editor.current.setCursor(lastCursorPos.current.line, lastCursorPos.current.ch);
}
};
eventBus.registerListener(QUERY_TOOL_EVENTS.EDITOR_LAST_FOCUS, lastFocus);
setTimeout(()=>{
(queryToolCtx.params.is_query_tool|| queryToolCtx.preferences.view_edit_promotion_warning) && editor.current.focus();
}, 250);
eventBus.registerListener(QUERY_TOOL_EVENTS.WARN_RELOAD_FILE, warnReloadFile);
eventBus.registerListener(QUERY_TOOL_EVENTS.TRIGGER_SAVE_QUERY_TOOL_DATA, ()=>{
setSaveQtData(true);
});
eventBus.registerListener(QUERY_TOOL_EVENTS.LOAD_SQL_FROM_LOCAL_STORAGE, (sqlId)=>{
let sqlValue = getToolData(sqlId);
if (sqlValue) {
eventBus.fireEvent(QUERY_TOOL_EVENTS.EDITOR_SET_SQL, sqlValue);
}
});
}, []);
useEffect(()=>{
@ -405,6 +442,11 @@ export default function Query({onTextSelect, setQtStatePartial}) {
const change = useCallback(()=>{
eventBus.fireEvent(QUERY_TOOL_EVENTS.QUERY_CHANGED, editor.current.isDirty());
if(isSaveToolDataEnabled('sqleditor') && editor.current.isDirty()){
eventBus.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_SAVE_QUERY_TOOL_DATA);
}
if(!queryToolCtx.params.is_query_tool && editor.current.isDirty()){
if(queryToolCtx.preferences.sqleditor.view_edit_promotion_warning){
checkViewEditDataPromotion();
@ -414,6 +456,15 @@ export default function Query({onTextSelect, setQtStatePartial}) {
}
}, []);
const [saveQtData, setSaveQtData] = useState(false);
useDelayDebounce(()=>{
let connectionInfo = { ..._.find(queryToolCtx.connection_list, c => c.is_selected),
'open_file_name':queryToolCtx.current_file, 'is_editor_dirty': editor.current.isDirty() };
saveToolData('sqleditor', connectionInfo, queryToolCtx.params.trans_id, editor.current.getValue());
setSaveQtData(false);
}, saveQtData, 500);
const closePromotionWarning = (closeModal)=>{
if(editor.current.isDirty()) {
editor.current.execCommand('undo');

View File

@ -9,9 +9,11 @@
import gettext from 'sources/gettext';
import url_for from 'sources/url_for';
import {getPanelTitle} from './sqleditor_title';
import {getPanelTitle, getTitle} from './sqleditor_title';
import {getRandomInt} from 'sources/utils';
import pgAdmin from 'sources/pgadmin';
import usePreferences from '../../../../preferences/static/js/store';
import pgWindow from 'sources/window';
function hasDatabaseInformation(parentData) {
return parentData.database;
@ -112,6 +114,33 @@ export function showERDSqlTool(parentData, erdSqlId, queryToolTitle, queryToolMo
launchQueryTool(queryToolMod, transId, gridUrl, queryToolTitle, {});
}
export function relaunchSqlTool(connectionInfo, sqlId){
let browserPref = usePreferences.getState().getPreferencesForModule('browser');
let parentData = {
server_group: {
_id: connectionInfo.sgid || 0,
},
server: {
_id: connectionInfo.sid,
label: connectionInfo.server,
},
database: {
_id: connectionInfo.did,
label: connectionInfo.database_name,
_label: connectionInfo.database_name,
},
};
const transId = getRandomInt(1, 9999999);
const qtUrl = generateUrl(transId, parentData, null);
const title = getTitle(pgAdmin, browserPref, parentData, false, connectionInfo.server_name, connectionInfo.database_name, connectionInfo.role || connectionInfo.user);
launchQueryTool(pgWindow.pgAdmin.Tools.SQLEditor, transId, qtUrl, title, {
sql_id: sqlId,
...connectionInfo,
});
}
export function launchQueryTool(queryToolMod, transId, gridUrl, queryToolTitle, params) {
let retVal = queryToolMod.launch(transId, gridUrl, true, queryToolTitle, params);

View File

@ -90,9 +90,10 @@ class PGUtilitiesMaintenanceFeatureTest(BaseFeatureTest):
def _open_maintenance_dialogue(self):
if self.test_level == 'table':
self.page.expand_tables_node("Server", self.server['name'],
self.server['db_password'],
self.database_name, 'public')
self.assertTrue(self.page.expand_tables_node(
"Server", self.server['name'], self.server['db_password'],
self.database_name, 'public'),
'Tree not expanded to the table node')
table_node = self.page.check_if_element_exists_with_scroll(
TreeAreaLocators.table_node(self.table_name))

View File

@ -32,9 +32,9 @@ class TableDdlFeatureTest(BaseFeatureTest):
secrets.choice(range(1000, 3000)))
test_utils.create_table(self.server, self.test_db,
self.test_table_name)
self.page.expand_tables_node("Server", self.server['name'],
self.server['db_password'], self.test_db,
'public')
self.assertTrue(self.page.expand_tables_node(
"Server", self.server['name'], self.server['db_password'],
self.test_db,'public'), 'Tree not expanded to the table node.')
table_node = self.page.check_if_element_exists_with_scroll(
TreeAreaLocators.table_node(self.test_table_name))

View File

@ -83,9 +83,11 @@ class CopySQLFeatureTest(BaseFeatureTest):
secrets.choice(range(1000, 3000)))
test_utils.create_table(self.server, self.test_db,
self.test_table_name)
self.page.expand_tables_node("Server", self.server['name'],
self.server['db_password'], self.test_db,
'public')
self.assertTrue(self.page.expand_tables_node(
"Server", self.server['name'], self.server['db_password'],
self.test_db, 'public'),
'Tree not expanded to the table node.')
table_node = self.page.check_if_element_exists_with_scroll(
TreeAreaLocators.table_node(self.test_table_name))
table_node.click()

View File

@ -115,10 +115,10 @@ CREATE TABLE public.nonintpkey
try:
self.page.wait_for_spinner_to_disappear()
self.page.add_server(self.server)
self.page.expand_tables_node("Server", self.server['name'],
self.server['db_password'],
self.test_db,
'public')
self.assertTrue(self.page.expand_tables_node(
"Server", self.server['name'], self.server['db_password'],
self.test_db, 'public'),
'Tree not expanded to the table node.')
self._load_config_data('table_insert_update_cases')
data_local = config_data

View File

@ -91,9 +91,10 @@ class CheckForXssFeatureTest(BaseFeatureTest):
self.server, self.test_db, self.test_table_name)
def _tables_node_expandable(self):
self.page.expand_tables_node("Server", self.server['name'],
self.server['db_password'], self.test_db,
'public')
self.assertTrue(self.page.expand_tables_node(
"Server", self.server['name'],self.server['db_password'],
self.test_db, 'public'),
'Tree not expanded to the table node.')
table_node = self.page.check_if_element_exists_with_scroll(
TreeAreaLocators.table_node(self.test_table_name))

View File

@ -152,6 +152,12 @@ class PgadminPage:
def open_query_tool(self):
self.click_element(self.find_by_css_selector(
"button[data-label='Tools']"))
WebDriverWait(self.driver, 3).until(
EC.visibility_of_element_located(
(By.CSS_SELECTOR, "li[data-label='Query Tool']")))
ActionChains(self.driver).move_to_element(
self.driver.find_element(
By.CSS_SELECTOR, "li[data-label='Query Tool']")).perform()
self.click_element(self.find_by_css_selector(
"li[data-label='Query Tool']"))
@ -502,7 +508,10 @@ class PgadminPage:
TreeAreaLocators.server_connection_status_element(server_name))
server_class = server_connection_status_element.get_attribute(
'class')
if server_class == 'icon-pg' or server_class == 'icon-ppas':
print("(click_expand_server_node)"
"server_class = " + str(server_class), file=sys.stderr)
if (server_class.find('icon-pg') != -1 or
server_class.find('icon-ppas') != -1):
server_connected = True
except Exception as e:
print("There is some exception thrown in the function "

View File

@ -23,6 +23,7 @@ import url_for from 'sources/url_for';
import Theme from '../../../pgadmin/static/js/Theme';
import SchemaDiffComponent from '../../../pgadmin/tools/schema_diff/static/js/components/SchemaDiffComponent';
import SchemaDiff from '../../../pgadmin/tools/schema_diff/static/js/SchemaDiffModule';
import { ApplicationStateProvider } from '../../../pgadmin/settings/static/ApplicationStateProvider';
describe('Schema Diff Component', () => {
@ -63,10 +64,12 @@ describe('Schema Diff Component', () => {
await act(async ()=>{
render(
<Theme>
<SchemaDiffComponent
params={{ transId: params.transId, pgAdmin: pgWindow.pgAdmin }}
>
</SchemaDiffComponent>
<ApplicationStateProvider>
<SchemaDiffComponent
params={{ transId: params.transId, pgAdmin: pgWindow.pgAdmin }}
>
</SchemaDiffComponent>
</ApplicationStateProvider>
</Theme>
);
});

View File

@ -955,6 +955,25 @@ def configure_preferences(default_binary_path=None):
('False', pref_breadcrumbs_enable.pid)
)
# Disable workspace save feature
misc_pref = Preferences.module('misc')
save_app_state = misc_pref.preference('save_app_state')
user_pref = cur.execute(
select_preference_query, (save_app_state.pid,)
)
if len(user_pref.fetchall()) == 0:
cur.execute(
insert_preferences_query,
(save_app_state.pid, 1, 'False')
)
else:
cur.execute(
update_preference_query,
('False', save_app_state.pid)
)
conn.commit()
conn.close()