Skip to content

Commit ae9bd5f

Browse files
Ensure worspaces/pgadmin state is saved on abrupt shutdown of pgadmin.#GH_3319
1 parent ec3d142 commit ae9bd5f

File tree

26 files changed

+658
-95
lines changed

26 files changed

+658
-95
lines changed
+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
2+
"""empty message
3+
4+
Revision ID: 4aa49c5d8eb1
5+
Revises: 1f0eddc8fc79
6+
Create Date: 2025-04-17 15:20:29.605023
7+
8+
"""
9+
import sqlalchemy as sa
10+
from alembic import op
11+
12+
# revision identifiers, used by Alembic.
13+
revision = '4aa49c5d8eb1'
14+
down_revision = '1f0eddc8fc79'
15+
branch_labels = None
16+
depends_on = None
17+
18+
19+
def upgrade():
20+
op.create_table(
21+
'pgadmin_state_data',
22+
sa.Column('uid', sa.Integer(), nullable=False),
23+
sa.Column('id', sa.Integer()),
24+
sa.Column('connection_info', sa.JSON()),
25+
sa.Column('tool_name', sa.String()),
26+
sa.Column('tool_data', sa.String()),
27+
sa.ForeignKeyConstraint(['uid'], ['user.id'], ondelete='CASCADE'),
28+
sa.PrimaryKeyConstraint('id', 'uid'))
29+
30+
31+
def downgrade():
32+
# pgAdmin only upgrades, downgrade not implemented.
33+
pass

web/pgadmin/browser/static/js/browser.js

+41-1
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,13 @@ import _ from 'lodash';
1212
import { checkMasterPassword, showQuickSearch } from '../../../static/js/Dialogs/index';
1313
import { pgHandleItemError } from '../../../static/js/utils';
1414
import { send_heartbeat, stop_heartbeat } from './heartbeat';
15-
import getApiInstance from '../../../static/js/api_instance';
15+
import getApiInstance, {parseApiError} from '../../../static/js/api_instance';
1616
import usePreferences, { setupPreferenceBroadcast } from '../../../preferences/static/js/store';
1717
import checkNodeVisibility from '../../../static/js/check_node_visibility';
18+
import * as showQueryTool from '../../../tools/sqleditor/static/js/show_query_tool';
19+
import {relaunchPsqlTool} from '../../../tools/psql/static/js/show_psql_tool';
20+
import {relaunchErdTool} from '../../../tools/erd/static/js/show_erd_tool';
21+
import { relaunchSchemaDiff } from '../../../tools/schema_diff/static/js/showSchemaDiffTool';
1822

1923
define('pgadmin.browser', [
2024
'sources/gettext', 'sources/url_for', 'sources/pgadmin',
@@ -206,6 +210,7 @@ define('pgadmin.browser', [
206210
uiloaded: function() {
207211
this.set_master_password('');
208212
this.check_version_update();
213+
this.restore_pgadmin_state();
209214
},
210215
check_corrupted_db_file: function() {
211216
getApiInstance().get(
@@ -291,6 +296,41 @@ define('pgadmin.browser', [
291296
});
292297
},
293298

299+
restore_pgadmin_state: async function () {
300+
getApiInstance().get(
301+
url_for('settings.get_pgadmin_state')
302+
).then((res)=> {
303+
if(res.data.success && res.data.data.result.length > 0){
304+
//let oldIds = []
305+
_.each(res.data.data.result, function(tool_data){
306+
if (tool_data.tool_name == 'sqleditor'){
307+
showQueryTool.relaunchSqlTool(tool_data);
308+
}else if(tool_data.tool_name == 'psql'){
309+
relaunchPsqlTool(tool_data);
310+
}else if(tool_data.tool_name == 'ERD'){
311+
relaunchErdTool(tool_data);
312+
}else if(tool_data.tool_name == 'schema_diff'){
313+
relaunchSchemaDiff(tool_data);
314+
}
315+
});
316+
317+
// call clear query data for which query tool has been launched.
318+
try {
319+
getApiInstance().delete(url_for('settings.delete_pgadmin_state'), {
320+
});
321+
} catch (error) {
322+
console.error(error);
323+
pgAdmin.Browser.notifier.error(gettext('Failed to remove query data.') + parseApiError(error));
324+
}
325+
326+
}
327+
}).catch(function(error) {
328+
pgAdmin.Browser.notifier.pgRespErrorNotify(error);
329+
});
330+
331+
},
332+
333+
294334
bind_beforeunload: function() {
295335
window.addEventListener('beforeunload', function(e) {
296336
/* Can open you in new tab */

web/pgadmin/misc/workspaces/static/js/WorkspaceProvider.jsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -75,16 +75,16 @@ export function WorkspaceProvider({children}) {
7575
pgAdmin.Browser.docker.currentWorkspace = newVal;
7676
if (newVal == WORKSPACES.DEFAULT) {
7777
setTimeout(() => {
78-
pgAdmin.Browser.tree.selectNode(lastSelectedTreeItem.current, true, 'center');
78+
pgAdmin.Browser.tree?.selectNode(lastSelectedTreeItem.current, true, 'center');
7979
lastSelectedTreeItem.current = null;
8080
}, 250);
8181
} else {
8282
// Get the selected tree node and save it into the state variable.
83-
let selItem = pgAdmin.Browser.tree.selected();
83+
let selItem = pgAdmin.Browser.tree?.selected();
8484
if (selItem)
8585
lastSelectedTreeItem.current = selItem;
8686
// Deselect the node to disable the menu options.
87-
pgAdmin.Browser.tree.deselect(selItem);
87+
pgAdmin.Browser.tree?.deselect(selItem);
8888
}
8989
setCurrentWorkspace(newVal);
9090
};

web/pgadmin/model/__init__.py

+12-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
#
3434
##########################################################################
3535

36-
SCHEMA_VERSION = 44
36+
SCHEMA_VERSION = 45
3737

3838
##########################################################################
3939
#
@@ -392,6 +392,17 @@ class QueryHistoryModel(db.Model):
392392
last_updated_flag = db.Column(db.String(), nullable=False)
393393

394394

395+
class PgadminStateData(db.Model):
396+
"""Define the history SQL table."""
397+
__tablename__ = 'pgadmin_state_data'
398+
uid = db.Column(db.Integer(), db.ForeignKey(USER_ID), nullable=False,
399+
primary_key=True)
400+
id = db.Column(db.Integer(),nullable=False,primary_key=True)
401+
connection_info = db.Column(MutableDict.as_mutable(types.JSON))
402+
tool_name = db.Column(db.String(), nullable=False)
403+
tool_data = db.Column(PgAdminDbBinaryString())
404+
405+
395406
class Database(db.Model):
396407
"""
397408
Define a Database.

web/pgadmin/settings/__init__.py

+113-2
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,10 @@
2121
success_return, internal_server_error
2222
from pgadmin.utils.menu import MenuItem
2323

24-
from pgadmin.model import db, Setting
24+
from pgadmin.model import db, Setting, PgadminStateData
2525
from pgadmin.utils.constants import MIMETYPE_APP_JS
2626
from .utils import get_dialog_type, get_file_type_setting
27+
from cryptography.fernet import Fernet
2728

2829
MODULE_NAME = 'settings'
2930

@@ -52,7 +53,9 @@ def get_exposed_url_endpoints(self):
5253
'settings.save_tree_state', 'settings.get_tree_state',
5354
'settings.reset_tree_state',
5455
'settings.save_file_format_setting',
55-
'settings.get_file_format_setting'
56+
'settings.get_file_format_setting',
57+
'settings.save_pgadmin_state',
58+
'settings.delete_pgadmin_state'
5659
]
5760

5861

@@ -256,3 +259,111 @@ def get_file_format_setting():
256259

257260
return make_json_response(success=True,
258261
info=get_file_type_setting(list(data.values())))
262+
263+
264+
@blueprint.route(
265+
'/save_pgadmin_state',
266+
methods=["POST"], endpoint='save_pgadmin_state'
267+
)
268+
@pga_login_required
269+
def save_pgadmin_state_data():
270+
"""
271+
Args:
272+
sid: server id
273+
did: database id
274+
"""
275+
data = json.loads(request.data)
276+
id = data['trans_id']
277+
fernet = Fernet(current_app.config['SECRET_KEY'].encode())
278+
tool_data = fernet.encrypt(json.dumps(data['tool_data']).encode())
279+
connection_info = data['connection_info'] \
280+
if 'connection_info' in data else None
281+
try:
282+
data_entry = PgadminStateData(
283+
uid=current_user.id, id=id,connection_info=connection_info,
284+
tool_name=data['tool_name'], tool_data=tool_data)
285+
286+
db.session.merge(data_entry)
287+
db.session.commit()
288+
except Exception as e:
289+
print(e)
290+
db.session.rollback()
291+
292+
return make_json_response(
293+
data={
294+
'status': True,
295+
'msg': 'Success',
296+
}
297+
)
298+
299+
300+
@blueprint.route(
301+
'/get_pgadmin_state',
302+
methods=["GET"], endpoint='get_pgadmin_state'
303+
)
304+
@pga_login_required
305+
def get_pgadmin_state():
306+
fernet = Fernet(current_app.config['SECRET_KEY'].encode())
307+
result = db.session \
308+
.query(PgadminStateData) \
309+
.filter(PgadminStateData.uid == current_user.id) \
310+
.all()
311+
312+
res = []
313+
for row in result:
314+
res.append({'tool_name': row.tool_name,
315+
'connection_info': row.connection_info,
316+
'tool_data': fernet.decrypt(row.tool_data).decode(),
317+
'id': row.id
318+
})
319+
return make_json_response(
320+
data={
321+
'status': True,
322+
'msg': '',
323+
'result': res
324+
}
325+
)
326+
327+
328+
@blueprint.route(
329+
'/delete_pgadmin_state/',
330+
methods=["DELETE"], endpoint='delete_pgadmin_state')
331+
@pga_login_required
332+
def delete_pgadmin_state_data():
333+
trans_id = None
334+
if request.data:
335+
data = json.loads(request.data)
336+
trans_id = int(data['panelId'].split('_')[-1])
337+
return delete_tool_data(trans_id)
338+
339+
340+
def delete_tool_data(trans_id):
341+
try:
342+
if trans_id:
343+
results = db.session \
344+
.query(PgadminStateData) \
345+
.filter(PgadminStateData.uid == current_user.id,
346+
PgadminStateData.id == trans_id) \
347+
.all()
348+
else:
349+
results = db.session \
350+
.query(PgadminStateData) \
351+
.filter(PgadminStateData.uid == current_user.id) \
352+
.all()
353+
for result in results:
354+
db.session.delete(result)
355+
db.session.commit()
356+
return make_json_response(
357+
data={
358+
'status': True,
359+
'msg': 'Success',
360+
}
361+
)
362+
except Exception as e:
363+
db.session.rollback()
364+
return make_json_response(
365+
data={
366+
'status': False,
367+
'msg': 'str(e)',
368+
}
369+
)

web/pgadmin/static/js/PgAdminProvider.jsx

+15-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99

1010
import React from 'react';
1111
import PropTypes from 'prop-types';
12+
import EventBus from './helpers/EventBus';
13+
import getApiInstance from './api_instance';
14+
import url_for from 'sources/url_for';
1215

1316
const PgAdminContext = React.createContext();
1417

@@ -19,7 +22,18 @@ export function usePgAdmin() {
1922

2023
export function PgAdminProvider({children, value}) {
2124

22-
return <PgAdminContext.Provider value={value}>
25+
const eventBus = React.useRef(new EventBus());
26+
27+
React.useEffect(()=>{
28+
eventBus.current.registerListener('SAVE_TOOL_DATA', (data) => {
29+
getApiInstance().post(
30+
url_for('settings.save_pgadmin_state'),
31+
JSON.stringify(data),
32+
).catch((error)=>{console.error(error);});
33+
});
34+
}, []);
35+
36+
return <PgAdminContext.Provider value={{pgAdminProviderEventBus: eventBus.current, ...value}}>
2337
{children}
2438
</PgAdminContext.Provider>;
2539
}

web/pgadmin/static/js/ToolView.jsx

+16
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ import { usePgAdmin } from './PgAdminProvider';
1313
import { BROWSER_PANELS } from '../../browser/static/js/constants';
1414
import PropTypes from 'prop-types';
1515
import LayoutIframeTab from './helpers/Layout/LayoutIframeTab';
16+
import { LAYOUT_EVENTS } from './helpers/Layout';
17+
import getApiInstance from './api_instance';
18+
import url_for from 'sources/url_for';
1619

1720
function ToolForm({actionUrl, params}) {
1821
const formRef = useRef(null);
@@ -56,6 +59,19 @@ export default function ToolView({dockerObj}) {
5659
// Handler here will return which layout instance the tool should go in
5760
// case of workspace layout.
5861
let handler = pgAdmin.Browser.getDockerHandler?.(panelId, dockerObj);
62+
const dergisterRemove = handler.docker.eventBus.registerListener(LAYOUT_EVENTS.REMOVE, (closePanelId)=>{
63+
if(panelId == closePanelId){
64+
let api = getApiInstance();
65+
api.delete(
66+
url_for('settings.delete_pgadmin_state'), {data:{'panelId': panelId}}
67+
).then(()=> { /* Sona qube */}).catch(function(error) {
68+
pgAdmin.Browser.notifier.pgRespErrorNotify(error);
69+
});
70+
dergisterRemove();
71+
}
72+
});
73+
74+
5975
handler.focus();
6076
handler.docker.openTab({
6177
id: panelId,

web/pgadmin/tools/erd/__init__.py

+8-8
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,15 @@
1010
"""A blueprint module implementing the erd tool."""
1111
import json
1212

13-
from flask import url_for, request, Response
13+
from flask import request, Response
1414
from flask import render_template, current_app as app
1515
from flask_security import permissions_required
1616
from pgadmin.user_login_check import pga_login_required
1717
from flask_babel import gettext
1818
from werkzeug.user_agent import UserAgent
1919
from pgadmin.utils import PgAdminModule, \
2020
SHORTCUT_FIELDS as shortcut_fields
21-
from pgadmin.utils.ajax import make_json_response, bad_request, \
22-
internal_server_error
21+
from pgadmin.utils.ajax import make_json_response, internal_server_error
2322
from pgadmin.model import Server
2423
from config import PG_DEFAULT_DRIVER
2524
from pgadmin.utils.driver import get_driver
@@ -29,13 +28,14 @@
2928
from pgadmin.browser.server_groups.servers.databases.schemas.tables. \
3029
constraints.foreign_key import utils as fkey_utils
3130
from pgadmin.utils.constants import PREF_LABEL_KEYBOARD_SHORTCUTS, \
32-
PREF_LABEL_DISPLAY, PREF_LABEL_OPTIONS
31+
PREF_LABEL_OPTIONS
3332
from .utils import ERDHelper
3433
from pgadmin.utils.exception import ConnectionLost
3534
from pgadmin.authenticate import socket_login_required
3635
from pgadmin.tools.user_management.PgAdminPermissions import AllPermissionTypes
3736
from ... import socketio
3837

38+
3939
MODULE_NAME = 'erd'
4040
SOCKETIO_NAMESPACE = '/{0}'.format(MODULE_NAME)
4141

@@ -462,11 +462,11 @@ def panel(trans_id):
462462
Args:
463463
panel_title: Title of the panel
464464
"""
465+
params = {'trans_id': trans_id, }
466+
if request.form:
467+
for key, val in request.form.items():
468+
params[key] = val
465469

466-
params = {
467-
'trans_id': trans_id,
468-
'title': request.form['title']
469-
}
470470
if request.args:
471471
params.update({k: v for k, v in request.args.items()})
472472

0 commit comments

Comments
 (0)