Skip to content

Commit c7f294c

Browse files
Fix some more review comments.
1 parent c443529 commit c7f294c

File tree

29 files changed

+496
-244
lines changed

29 files changed

+496
-244
lines changed
14.1 KB
Loading

docs/en_US/preferences.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,12 +309,21 @@ Use the fields on the *User Interface* panel to set the user interface related p
309309
this setting is False, meaning that Query Tool/PSQL tabs will open in the currently
310310
active workspace (either the default or the workspace selected at the time of opening).
311311

312+
* When the *Save the application state?* option is enabled the current state of various
313+
tools—such as Query Tool, ERD, Schema Diff, and PSQL—will be saved in the encrypted
314+
format.If the application is closed unexpectedly, the tab is accidentally closed,
315+
or the page is refreshed, the saved state will be automatically restored for
316+
each respective tool.**Note:**
317+
312318
* Use the *Themes* drop-down listbox to select the theme for pgAdmin. You'll also get a preview just below the
313319
drop down. You can also submit your own themes,
314320
check `here <https://github.com/pgadmin-org/pgadmin4/blob/master/README.md>`_ how.
315321
Currently we support Light, Dark, High Contrast and System theme. Selecting System option will follow
316322
your computer's settings.
317323

324+
**Note:** Saving the application state will not preserve data for tool tabs opened in
325+
separate browser tabs when running in server mode..
326+
318327
The Paths Node
319328
**************
320329

web/pgadmin/authenticate/__init__.py

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -285,8 +285,6 @@ def authenticate(self):
285285
source.get_source_name())
286286

287287
status, msg = source.authenticate(self.form)
288-
print(status)
289-
print(msg)
290288

291289
if status:
292290
self.set_current_source(source.get_source_name())
@@ -297,17 +295,6 @@ def authenticate(self):
297295
current_app.logger.debug(
298296
"Authentication initiated via source: %s is failed." %
299297
source.get_source_name())
300-
current_user = User.query.filter_by(username=username,
301-
auth_source=src).first()
302-
# get list with auth source src
303-
# iterate over it and find if user present with that
304-
if len(users) > 0 and current_user:
305-
print('User may be coming first time so continue with all possible')
306-
break
307-
elif len(users) > 0:
308-
print('User already present with other aut source break it')
309-
break
310-
311298
return status, msg
312299

313300
def login(self):

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

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ define('pgadmin.browser', [
212212
let save_the_workspace = prefStore.getPreferencesForModule('misc').save_app_state;
213213
if(save_the_workspace){
214214
this.restore_pgadmin_state();
215+
pgBrowser.docker.default_workspace.focus();
215216
}
216217
},
217218
check_corrupted_db_file: function() {
@@ -299,33 +300,33 @@ define('pgadmin.browser', [
299300
},
300301

301302
restore_pgadmin_state: function () {
302-
getApiInstance().get(
303+
getApiInstance({'Content-Encoding': 'gzip'}).get(
303304
url_for('settings.get_application_state')
304305
).then((res)=> {
305306
if(res.data.success && res.data.data.result.length > 0){
306-
_.each(res.data.data.result, function(tool_state){
307-
let tool_name = tool_state.tool_name;
308-
let tool_data = tool_state.tool_data;
309-
let sql_id = `${tool_name}-${getRandomInt(1, 9999999)}`;
310-
311-
if (tool_name == 'sqleditor'){
312-
localStorage.setItem(sql_id, tool_data);
313-
showQueryTool.relaunchSqlTool(tool_state, sql_id);
314-
}else if(tool_name == 'psql'){
315-
pgAdmin.Tools.Psql.openPsqlTool(null, null, tool_state);
316-
}else if(tool_name == 'ERD'){
317-
localStorage.setItem(sql_id, tool_data);
318-
pgAdmin.Tools.ERD.showErdTool(null, null, false, sql_id, tool_state);
319-
}else if(tool_name == 'schema_diff'){
320-
localStorage.setItem(sql_id, tool_data);
321-
pgAdmin.Tools.SchemaDiff.launchSchemaDiff(sql_id);
307+
_.each(res.data.data.result, function(toolState){
308+
let toolNme = toolState.tool_name;
309+
let toolData = toolState.tool_data;
310+
let toolDataId = `${toolNme}-${getRandomInt(1, 9999999)}`;
311+
let connectionInfo = toolState.connection_info;
312+
313+
if (toolNme == 'sqleditor'){
314+
localStorage.setItem(toolDataId, toolData);
315+
showQueryTool.relaunchSqlTool(connectionInfo, toolDataId);
316+
}else if(toolNme == 'psql'){
317+
pgAdmin.Tools.Psql.openPsqlTool(null, null, connectionInfo);
318+
}else if(toolNme == 'ERD'){
319+
localStorage.setItem(toolDataId, toolData);
320+
pgAdmin.Tools.ERD.showErdTool(null, null, false, connectionInfo, toolDataId);
321+
}else if(toolNme == 'schema_diff'){
322+
localStorage.setItem(toolDataId, toolData);
323+
pgAdmin.Tools.SchemaDiff.launchSchemaDiff(toolDataId);
322324
}
323325
});
324326

325327
// call clear application state data.
326328
try {
327-
getApiInstance().delete(url_for('settings.delete_application_state'), {
328-
});
329+
getApiInstance().delete(url_for('settings.delete_application_state'), {});
329330
} catch (error) {
330331
console.error(error);
331332
pgAdmin.Browser.notifier.error(gettext('Failed to remove query data.') + parseApiError(error));

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -455,7 +455,7 @@ export default function AdHocConnection({mode}) {
455455
'pgadmin:tool:show',
456456
`${BROWSER_PANELS.PSQL_TOOL}_${transId}`,
457457
openUrl,
458-
{title: escapedTitle, db: db_name},
458+
{title: escapedTitle, db: db_name, server_name: formData.server_name, 'user': user_name},
459459
{title: panelTitle, icon: 'pg-font-icon icon-terminal', manualClose: false, renamable: true},
460460
Boolean(open_new_tab?.includes('psql_tool'))
461461
);

web/pgadmin/preferences/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
import json
1717
from flask import render_template, Response, request, session, current_app
1818
from flask_babel import gettext
19+
20+
from pgadmin.settings import delete_tool_data
1921
from pgadmin.user_login_check import pga_login_required
2022
from pgadmin.utils import PgAdminModule
2123
from pgadmin.utils.ajax import success_return, \
@@ -238,6 +240,9 @@ def save():
238240
data['mid'], data['category_id'], data['id'], data['value'])
239241
sgm.get_nodes(sgm)
240242

243+
if data['name'] == 'save_app_state' and not data['value']:
244+
delete_tool_data()
245+
241246
if not res:
242247
return internal_server_error(errormsg=msg)
243248

web/pgadmin/settings/__init__.py

Lines changed: 107 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,17 @@
88
##########################################################################
99

1010
"""Utility functions for storing and retrieving user configuration settings."""
11-
11+
import os
1212
import traceback
1313
import json
1414

1515
from flask import Response, request, render_template, url_for, current_app
1616
from flask_babel import gettext
1717
from flask_login import current_user
18+
from selenium.webdriver.support.expected_conditions import \
19+
element_selection_state_to_be
20+
from sqlalchemy import false
21+
1822
from pgadmin.user_login_check import pga_login_required
1923
from pgadmin.utils import PgAdminModule
2024
from pgadmin.utils.ajax import make_json_response, bad_request,\
@@ -262,26 +266,32 @@ def get_file_format_setting():
262266
info=get_file_type_setting(list(data.values())))
263267

264268

269+
270+
265271
@blueprint.route(
266272
'/save_application_state',
267273
methods=["POST"], endpoint='save_application_state'
268274
)
269275
@pga_login_required
270276
def save_application_state():
271277
"""
272-
Args:
273-
sid: server id
274-
did: database id
278+
Expose an api to save the application state which stores the data from
279+
query tool, ERD, schema-diff, psql
275280
"""
276281
data = json.loads(request.data)
277-
id = data['trans_id']
282+
trans_id = data['trans_id']
278283
fernet = Fernet(current_app.config['SECRET_KEY'].encode())
279284
tool_data = fernet.encrypt(json.dumps(data['tool_data']).encode())
280285
connection_info = data['connection_info'] \
281286
if 'connection_info' in data else None
287+
if 'open_file_name' in connection_info and connection_info['open_file_name'] and 'is_editor_dirty' in connection_info and connection_info['is_editor_dirty']:
288+
last_saved_file_hash = compute_sha256_large_file(connection_info['open_file_name'])
289+
print(last_saved_file_hash)
290+
connection_info['last_saved_file_hash'] = last_saved_file_hash
291+
282292
try:
283293
data_entry = ApplicationState(
284-
uid=current_user.id, id=id,connection_info=connection_info,
294+
uid=current_user.id, id=trans_id,connection_info=connection_info,
285295
tool_name=data['tool_name'], tool_data=tool_data)
286296

287297
db.session.merge(data_entry)
@@ -304,6 +314,9 @@ def save_application_state():
304314
)
305315
@pga_login_required
306316
def get_application_state():
317+
"""
318+
Returns application state if any stored.
319+
"""
307320
fernet = Fernet(current_app.config['SECRET_KEY'].encode())
308321
result = db.session \
309322
.query(ApplicationState) \
@@ -312,8 +325,26 @@ def get_application_state():
312325

313326
res = []
314327
for row in result:
328+
connection_info = row.connection_info
329+
print(connection_info)
330+
if 'open_file_name' in connection_info and connection_info['open_file_name']:
331+
file_path = connection_info['open_file_name']
332+
file_deleted = False if os.path.exists(file_path) else True
333+
connection_info['file_deleted'] = file_deleted
334+
if not file_deleted and connection_info['is_editor_dirty']:
335+
if 'last_saved_file_hash' in connection_info and connection_info['last_saved_file_hash']:
336+
connection_info['external_file_changes'] = check_external_file_changes(file_path, connection_info['last_saved_file_hash'])
337+
338+
339+
# if 'open_file_name' in connection_info and connection_info['open_file_name']:
340+
# initial_file_hash = connection_info['initial_file_hash']
341+
# file_deleted, file_modified_in_pgadmin, file_modified_externally = detect_file_change(connection_info['open_file_name'], tool_data, initial_file_hash )
342+
# connection_info['file_deleted'] = file_deleted
343+
# connection_info['file_modified_in_pgadmin'] = file_modified_in_pgadmin
344+
# connection_info['file_modified_externally'] = file_modified_externally
345+
315346
res.append({'tool_name': row.tool_name,
316-
'connection_info': row.connection_info,
347+
'connection_info': connection_info,
317348
'tool_data': fernet.decrypt(row.tool_data).decode(),
318349
'id': row.id
319350
})
@@ -335,10 +366,16 @@ def delete_application_state():
335366
if request.data:
336367
data = json.loads(request.data)
337368
trans_id = int(data['panelId'].split('_')[-1])
338-
return delete_tool_data(trans_id)
369+
status, msg = delete_tool_data(trans_id)
370+
return make_json_response(
371+
data={
372+
'status': status,
373+
'msg': msg,
374+
}
375+
)
339376

340377

341-
def delete_tool_data(trans_id):
378+
def delete_tool_data(trans_id=None):
342379
try:
343380
if trans_id:
344381
results = db.session \
@@ -354,17 +391,66 @@ def delete_tool_data(trans_id):
354391
for result in results:
355392
db.session.delete(result)
356393
db.session.commit()
357-
return make_json_response(
358-
data={
359-
'status': True,
360-
'msg': 'Success',
361-
}
362-
)
394+
return True, 'Success'
363395
except Exception as e:
364396
db.session.rollback()
365-
return make_json_response(
366-
data={
367-
'status': False,
368-
'msg': str(e),
369-
}
370-
)
397+
return False, str(e)
398+
399+
400+
import hashlib
401+
402+
def compute_sha256_large_data_in_memory(data):
403+
"""Hash large data (in-memory) by processing in chunks."""
404+
sha256_hash = hashlib.sha256()
405+
# Process data in 8 KB chunks
406+
string_data = json.loads(data)
407+
chunk_size = 8192
408+
for i in range(0, len(string_data), chunk_size):
409+
chunk = string_data[i:i + chunk_size]
410+
sha256_hash.update(chunk.encode("utf-8"))
411+
412+
return sha256_hash.hexdigest()
413+
414+
415+
def compute_sha256_large_file(file_path):
416+
"""Compute SHA-256 hash for large files by reading in chunks."""
417+
sha256_hash = hashlib.sha256()
418+
419+
# Open the file in binary mode
420+
with open(file_path, "rb") as file:
421+
# Read and hash in 8 KB chunks (can adjust the chunk size if needed)
422+
for chunk in iter(lambda: file.read(8192), b""):
423+
print(chunk)
424+
sha256_hash.update(chunk)
425+
426+
return sha256_hash.hexdigest()
427+
428+
429+
def detect_file_change(file_path, data, initial_file_hash ):
430+
file_deleted = False
431+
file_modified_in_pgadmin = False
432+
file_modified_externally = False
433+
if os.path.exists(file_path):
434+
current_file_hash = compute_sha256_large_file(file_path)
435+
stored_data_hash = compute_sha256_large_data_in_memory(data)
436+
if stored_data_hash != current_file_hash:
437+
if stored_data_hash != initial_file_hash:
438+
# file changes in pgadmin
439+
file_modified_in_pgadmin = True
440+
441+
if current_file_hash != initial_file_hash:
442+
# file is changed externally
443+
file_modified_externally = True
444+
445+
else:
446+
file_deleted = True
447+
file_modified_in_pgadmin = True
448+
return file_deleted, file_modified_in_pgadmin, file_modified_externally
449+
450+
451+
def check_external_file_changes(file_path, last_saved_file_hash):
452+
current_file_hash = compute_sha256_large_file(file_path)
453+
if current_file_hash != last_saved_file_hash:
454+
return True
455+
return False
456+

web/pgadmin/settings/static/ApplicationStateProvider.jsx

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,27 +10,48 @@ import React, { useContext, useMemo } from 'react';
1010
import PropTypes from 'prop-types';
1111
import getApiInstance from '../../static/js/api_instance';
1212
import url_for from 'sources/url_for';
13+
import { getBrowser } from '../../static/js/utils';
14+
import usePreferences from '../../preferences/static/js/store';
1315

1416
const ApplicationStateContext = React.createContext();
1517

1618
export const useApplicationState = ()=>useContext(ApplicationStateContext);
1719

18-
export function retrieveDataFromLocalStorgae(sqlId){
19-
let sqlValue = JSON.parse(localStorage.getItem(sqlId));
20-
localStorage.removeItem(sqlId);
21-
return sqlValue;
20+
export function getToolData(localStorageId){
21+
let toolDataJson = JSON.parse(localStorage.getItem(localStorageId));
22+
localStorage.removeItem(localStorageId);
23+
return toolDataJson;
2224
}
2325

2426
export function ApplicationStateProvider({children}){
25-
const saveToolData = (data) =>{
26-
getApiInstance().post(
27+
const preferencesStore = usePreferences();
28+
const saveAppState = preferencesStore?.getPreferencesForModule('misc')?.save_app_state;
29+
const openNewTab = preferencesStore?.getPreferencesForModule('browser')?.new_browser_tab_open;
30+
31+
const saveToolData = (toolName, connectionInfo, transId, toolData) =>{
32+
let data = {
33+
'tool_name': toolName,
34+
'connection_info': connectionInfo,
35+
'trans_id': transId,
36+
'tool_data': toolData
37+
};
38+
getApiInstance({'Content-Encoding': 'gzip'}).post(
2739
url_for('settings.save_application_state'),
2840
JSON.stringify(data),
2941
).catch((error)=>{console.error(error);});
3042
};
3143

44+
const enableSaveToolData = (toolName)=>{
45+
let toolMapping = {'sqleditor': 'qt', 'schema_diff': 'schema_diff', 'psql': 'psql_tool', 'ERD': 'erd_tool'};
46+
if(openNewTab?.includes(toolMapping[toolName])){
47+
return saveAppState && getBrowser().name == 'Electron';
48+
}
49+
return saveAppState;
50+
};
51+
3252
const value = useMemo(()=>({
3353
saveToolData,
54+
enableSaveToolData
3455
}), []);
3556

3657
return <ApplicationStateContext.Provider value={value}>

0 commit comments

Comments
 (0)