diff --git a/docs/en_US/images/preferences_misc_user_interface.png b/docs/en_US/images/preferences_misc_user_interface.png index 34a3c9b4d..841f9d918 100644 Binary files a/docs/en_US/images/preferences_misc_user_interface.png and b/docs/en_US/images/preferences_misc_user_interface.png differ diff --git a/docs/en_US/preferences.rst b/docs/en_US/preferences.rst index 46e03279a..93138beab 100644 --- a/docs/en_US/preferences.rst +++ b/docs/en_US/preferences.rst @@ -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 `_ 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 ************** diff --git a/web/migrations/versions/c62bcc14c3d6_.py b/web/migrations/versions/c62bcc14c3d6_.py index 0b3cfcf76..2f0cd36f6 100644 --- a/web/migrations/versions/c62bcc14c3d6_.py +++ b/web/migrations/versions/c62bcc14c3d6_.py @@ -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. diff --git a/web/pgadmin/authenticate/__init__.py b/web/pgadmin/authenticate/__init__.py index aa3d75ac8..bb1e4dbd7 100644 --- a/web/pgadmin/authenticate/__init__.py +++ b/web/pgadmin/authenticate/__init__.py @@ -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): diff --git a/web/pgadmin/browser/static/js/browser.js b/web/pgadmin/browser/static/js/browser.js index f6f1d6626..81afb051d 100644 --- a/web/pgadmin/browser/static/js/browser.js +++ b/web/pgadmin/browser/static/js/browser.js @@ -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 */ diff --git a/web/pgadmin/misc/__init__.py b/web/pgadmin/misc/__init__.py index 96c1fbb2e..eda2a380c 100644 --- a/web/pgadmin/misc/__init__.py +++ b/web/pgadmin/misc/__init__.py @@ -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', diff --git a/web/pgadmin/misc/workspaces/static/js/AdHocConnection.jsx b/web/pgadmin/misc/workspaces/static/js/AdHocConnection.jsx index 0edad860e..4df84a351 100644 --- a/web/pgadmin/misc/workspaces/static/js/AdHocConnection.jsx +++ b/web/pgadmin/misc/workspaces/static/js/AdHocConnection.jsx @@ -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')) ); diff --git a/web/pgadmin/misc/workspaces/static/js/WorkspaceProvider.jsx b/web/pgadmin/misc/workspaces/static/js/WorkspaceProvider.jsx index f51df2578..6839d9e9b 100644 --- a/web/pgadmin/misc/workspaces/static/js/WorkspaceProvider.jsx +++ b/web/pgadmin/misc/workspaces/static/js/WorkspaceProvider.jsx @@ -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); }; diff --git a/web/pgadmin/model/__init__.py b/web/pgadmin/model/__init__.py index 13f34aa8a..32ff8d236 100644 --- a/web/pgadmin/model/__init__.py +++ b/web/pgadmin/model/__init__.py @@ -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. diff --git a/web/pgadmin/preferences/__init__.py b/web/pgadmin/preferences/__init__.py index f8748c74c..da9974595 100644 --- a/web/pgadmin/preferences/__init__.py +++ b/web/pgadmin/preferences/__init__.py @@ -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) diff --git a/web/pgadmin/settings/__init__.py b/web/pgadmin/settings/__init__.py index 759209f4b..b91d4efc1 100644 --- a/web/pgadmin/settings/__init__.py +++ b/web/pgadmin/settings/__init__.py @@ -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 diff --git a/web/pgadmin/settings/static/ApplicationStateProvider.jsx b/web/pgadmin/settings/static/ApplicationStateProvider.jsx new file mode 100644 index 000000000..6af331b95 --- /dev/null +++ b/web/pgadmin/settings/static/ApplicationStateProvider.jsx @@ -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 + {children} + ; + +} + +ApplicationStateProvider.propTypes = { + children: PropTypes.object +}; \ No newline at end of file diff --git a/web/pgadmin/static/js/PgAdminProvider.jsx b/web/pgadmin/static/js/PgAdminProvider.jsx index a28c07941..1a6efde7d 100644 --- a/web/pgadmin/static/js/PgAdminProvider.jsx +++ b/web/pgadmin/static/js/PgAdminProvider.jsx @@ -18,7 +18,6 @@ export function usePgAdmin() { } export function PgAdminProvider({children, value}) { - return {children} ; diff --git a/web/pgadmin/static/js/ToolErrorView.jsx b/web/pgadmin/static/js/ToolErrorView.jsx new file mode 100644 index 000000000..f043b799c --- /dev/null +++ b/web/pgadmin/static/js/ToolErrorView.jsx @@ -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 ( + {HTMLReactParse(err_msg)} + ); +} + +ToolErrorView.propTypes = { + error: PropTypes.string, + panelId: PropTypes.string, + panelDocker: PropTypes.object, + pgAdmin: PropTypes.object, + toolName: PropTypes.string, +}; \ No newline at end of file diff --git a/web/pgadmin/static/js/ToolView.jsx b/web/pgadmin/static/js/ToolView.jsx index 533b7f690..aa1947f3e 100644 --- a/web/pgadmin/static/js/ToolView.jsx +++ b/web/pgadmin/static/js/ToolView.jsx @@ -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, diff --git a/web/pgadmin/static/js/helpers/Layout/index.jsx b/web/pgadmin/static/js/helpers/Layout/index.jsx index eec050b2b..fd8688878 100644 --- a/web/pgadmin/static/js/helpers/Layout/index.jsx +++ b/web/pgadmin/static/js/helpers/Layout/index.jsx @@ -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 && <> - + + + } ); diff --git a/web/pgadmin/tools/erd/__init__.py b/web/pgadmin/tools/erd/__init__.py index 39d1feeab..0a9eb8c96 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 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( diff --git a/web/pgadmin/tools/erd/static/js/ERDModule.js b/web/pgadmin/tools/erd/static/js/ERDModule.js index 51b8378ef..0c8d3a5aa 100644 --- a/web/pgadmin/tools/erd/static/js/ERDModule.js +++ b/web/pgadmin/tools/erd/static/js/ERDModule.js @@ -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( - - - - + + + + { params.error ? + : + } + + ); } -} +} \ No newline at end of file diff --git a/web/pgadmin/tools/erd/static/js/erd_tool/ERDConstants.js b/web/pgadmin/tools/erd/static/js/erd_tool/ERDConstants.js index f7f6cd3d8..e9b7af1f9 100644 --- a/web/pgadmin/tools/erd/static/js/erd_tool/ERDConstants.js +++ b/web/pgadmin/tools/erd/static/js/erd_tool/ERDConstants.js @@ -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' }; 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 429a0b0ad..89f0fcf3c 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 @@ -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)}/> @@ -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, diff --git a/web/pgadmin/tools/erd/static/js/erd_tool/components/MainToolBar.jsx b/web/pgadmin/tools/erd/static/js/erd_tool/components/MainToolBar.jsx index a2bc51837..e71e648eb 100644 --- a/web/pgadmin/tools/erd/static/js/erd_tool/components/MainToolBar.jsx +++ b/web/pgadmin/tools/erd/static/js/erd_tool/components/MainToolBar.jsx @@ -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); diff --git a/web/pgadmin/tools/psql/__init__.py b/web/pgadmin/tools/psql/__init__.py index ca96fcad6..c50d05a8a 100644 --- a/web/pgadmin/tools/psql/__init__.py +++ b/web/pgadmin/tools/psql/__init__.py @@ -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 diff --git a/web/pgadmin/tools/psql/static/js/PsqlModule.js b/web/pgadmin/tools/psql/static/js/PsqlModule.js index cfe0309a0..553b02b50 100644 --- a/web/pgadmin/tools/psql/static/js/PsqlModule.js +++ b/web/pgadmin/tools/psql/static/js/PsqlModule.js @@ -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( - - - - + + + + { params.error ? + : + + } + + ); } - - - } diff --git a/web/pgadmin/tools/psql/static/js/components/PsqlComponent.jsx b/web/pgadmin/tools/psql/static/js/components/PsqlComponent.jsx index 5d1918800..c5b435efb 100644 --- a/web/pgadmin/tools/psql/static/js/components/PsqlComponent.jsx +++ b/web/pgadmin/tools/psql/static/js/components/PsqlComponent.jsx @@ -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 ( @@ -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, }; diff --git a/web/pgadmin/tools/schema_diff/__init__.py b/web/pgadmin/tools/schema_diff/__init__.py index 0d4747de9..1214535b6 100644 --- a/web/pgadmin/tools/schema_diff/__init__.py +++ b/web/pgadmin/tools/schema_diff/__init__.py @@ -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//', - 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) ) diff --git a/web/pgadmin/tools/schema_diff/static/js/SchemaDiffModule.js b/web/pgadmin/tools/schema_diff/static/js/SchemaDiffModule.js index 7240d7aa5..0db4a13f4 100644 --- a/web/pgadmin/tools/schema_diff/static/js/SchemaDiffModule.js +++ b/web/pgadmin/tools/schema_diff/static/js/SchemaDiffModule.js @@ -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( - - - - + + + + + + ); diff --git a/web/pgadmin/tools/schema_diff/static/js/components/InputComponent.jsx b/web/pgadmin/tools/schema_diff/static/js/components/InputComponent.jsx index 980439158..855e8120c 100644 --- a/web/pgadmin/tools/schema_diff/static/js/components/InputComponent.jsx +++ b/web/pgadmin/tools/schema_diff/static/js/components/InputComponent.jsx @@ -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); diff --git a/web/pgadmin/tools/schema_diff/static/js/components/SchemaDiffCompare.jsx b/web/pgadmin/tools/schema_diff/static/js/components/SchemaDiffCompare.jsx index 8f9d4f329..a397257f7 100644 --- a/web/pgadmin/tools/schema_diff/static/js/components/SchemaDiffCompare.jsx +++ b/web/pgadmin/tools/schema_diff/static/js/components/SchemaDiffCompare.jsx @@ -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 ( diff --git a/web/pgadmin/tools/schema_diff/static/js/components/SchemaDiffComponent.jsx b/web/pgadmin/tools/schema_diff/static/js/components/SchemaDiffComponent.jsx index ba42dbe13..adaac53b0 100644 --- a/web/pgadmin/tools/schema_diff/static/js/components/SchemaDiffComponent.jsx +++ b/web/pgadmin/tools/schema_diff/static/js/components/SchemaDiffComponent.jsx @@ -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(); diff --git a/web/pgadmin/tools/schema_diff/templates/schema_diff/index.html b/web/pgadmin/tools/schema_diff/templates/schema_diff/index.html index d545b5941..ecf6b196d 100644 --- a/web/pgadmin/tools/schema_diff/templates/schema_diff/index.html +++ b/web/pgadmin/tools/schema_diff/templates/schema_diff/index.html @@ -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); diff --git a/web/pgadmin/tools/sqleditor/__init__.py b/web/pgadmin/tools/sqleditor/__init__.py index 85ed1e063..981dfbe7c 100644 --- a/web/pgadmin/tools/sqleditor/__init__.py +++ b/web/pgadmin/tools/sqleditor/__init__.py @@ -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)) diff --git a/web/pgadmin/tools/sqleditor/static/js/SQLEditorModule.js b/web/pgadmin/tools/sqleditor/static/js/SQLEditorModule.js index 75fb850c4..b50c89b74 100644 --- a/web/pgadmin/tools/sqleditor/static/js/SQLEditorModule.js +++ b/web/pgadmin/tools/sqleditor/static/js/SQLEditorModule.js @@ -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( - - - - + + + + { params.error ? + : + } + + ); diff --git a/web/pgadmin/tools/sqleditor/static/js/components/QueryToolComponent.jsx b/web/pgadmin/tools/sqleditor/static/js/components/QueryToolComponent.jsx index eba3cdac7..894520061 100644 --- a/web/pgadmin/tools/sqleditor/static/js/components/QueryToolComponent.jsx +++ b/web/pgadmin/tools/sqleditor/static/js/components/QueryToolComponent.jsx @@ -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, diff --git a/web/pgadmin/tools/sqleditor/static/js/components/QueryToolConstants.js b/web/pgadmin/tools/sqleditor/static/js/components/QueryToolConstants.js index 117650ff0..3b41efcf2 100644 --- a/web/pgadmin/tools/sqleditor/static/js/components/QueryToolConstants.js +++ b/web/pgadmin/tools/sqleditor/static/js/components/QueryToolConstants.js @@ -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', diff --git a/web/pgadmin/tools/sqleditor/static/js/components/sections/Query.jsx b/web/pgadmin/tools/sqleditor/static/js/components/sections/Query.jsx index 9833cf9fa..fd81247a9 100644 --- a/web/pgadmin/tools/sqleditor/static/js/components/sections/Query.jsx +++ b/web/pgadmin/tools/sqleditor/static/js/components/sections/Query.jsx @@ -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'); diff --git a/web/pgadmin/tools/sqleditor/static/js/show_query_tool.js b/web/pgadmin/tools/sqleditor/static/js/show_query_tool.js index a26cedeb4..02b0e7441 100644 --- a/web/pgadmin/tools/sqleditor/static/js/show_query_tool.js +++ b/web/pgadmin/tools/sqleditor/static/js/show_query_tool.js @@ -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); diff --git a/web/regression/feature_tests/pg_utilities_maintenance_test.py b/web/regression/feature_tests/pg_utilities_maintenance_test.py index 38568118a..886cde4fd 100644 --- a/web/regression/feature_tests/pg_utilities_maintenance_test.py +++ b/web/regression/feature_tests/pg_utilities_maintenance_test.py @@ -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)) diff --git a/web/regression/feature_tests/table_ddl_feature_test.py b/web/regression/feature_tests/table_ddl_feature_test.py index 89b643b3b..c806a37a1 100644 --- a/web/regression/feature_tests/table_ddl_feature_test.py +++ b/web/regression/feature_tests/table_ddl_feature_test.py @@ -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)) diff --git a/web/regression/feature_tests/test_copy_sql_to_query_tool.py b/web/regression/feature_tests/test_copy_sql_to_query_tool.py index 8a6429ae2..c40a3387d 100644 --- a/web/regression/feature_tests/test_copy_sql_to_query_tool.py +++ b/web/regression/feature_tests/test_copy_sql_to_query_tool.py @@ -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() diff --git a/web/regression/feature_tests/view_data_dml_queries.py b/web/regression/feature_tests/view_data_dml_queries.py index 680b3c087..0e813ca8c 100644 --- a/web/regression/feature_tests/view_data_dml_queries.py +++ b/web/regression/feature_tests/view_data_dml_queries.py @@ -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 diff --git a/web/regression/feature_tests/xss_checks_panels_and_query_tool_test.py b/web/regression/feature_tests/xss_checks_panels_and_query_tool_test.py index 8265957c6..d0123ac12 100644 --- a/web/regression/feature_tests/xss_checks_panels_and_query_tool_test.py +++ b/web/regression/feature_tests/xss_checks_panels_and_query_tool_test.py @@ -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)) diff --git a/web/regression/feature_utils/pgadmin_page.py b/web/regression/feature_utils/pgadmin_page.py index d54a90b1d..69e9e6757 100644 --- a/web/regression/feature_utils/pgadmin_page.py +++ b/web/regression/feature_utils/pgadmin_page.py @@ -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 " diff --git a/web/regression/javascript/schema_diff/schema_diff_spec.js b/web/regression/javascript/schema_diff/schema_diff_spec.js index a586779e0..8495d7531 100644 --- a/web/regression/javascript/schema_diff/schema_diff_spec.js +++ b/web/regression/javascript/schema_diff/schema_diff_spec.js @@ -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( - - + + + + ); }); diff --git a/web/regression/python_test_utils/test_utils.py b/web/regression/python_test_utils/test_utils.py index 397118984..7ad80d96b 100644 --- a/web/regression/python_test_utils/test_utils.py +++ b/web/regression/python_test_utils/test_utils.py @@ -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()