Skip to content

Commit 7441c9e

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

File tree

27 files changed

+707
-127
lines changed

27 files changed

+707
-127
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

+45-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,11 @@ define('pgadmin.browser', [
206210
uiloaded: function() {
207211
this.set_master_password('');
208212
this.check_version_update();
213+
const prefStore = usePreferences.getState();
214+
let save_the_workspace = prefStore.getPreferencesForModule('misc').save_the_workspace;
215+
if(save_the_workspace){
216+
this.restore_pgadmin_state();
217+
}
209218
},
210219
check_corrupted_db_file: function() {
211220
getApiInstance().get(
@@ -291,6 +300,41 @@ define('pgadmin.browser', [
291300
});
292301
},
293302

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

web/pgadmin/misc/__init__.py

+13
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,19 @@ def register_preferences(self):
124124
)
125125
)
126126

127+
self.preference.register(
128+
'user_interface', 'save_the_workspace',
129+
gettext("Save the workspaces and data?"),
130+
'boolean', True,
131+
category_label=PREF_LABEL_USER_INTERFACE,
132+
help_str=gettext(
133+
'Turning on this setting will save the information about the'
134+
' tools that are opened and data within it. Data will be '
135+
'restored in case of abrupt shutdown/refresh of the '
136+
'application'
137+
)
138+
)
139+
127140
if not config.SERVER_MODE:
128141
self.preference.register(
129142
'file_downloads', 'automatically_open_downloaded_file',

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,

0 commit comments

Comments
 (0)