Skip to content

Commit 13d2553

Browse files
ab
1 parent a5a8ef1 commit 13d2553

File tree

9 files changed

+144
-108
lines changed

9 files changed

+144
-108
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,7 @@ define('pgadmin.browser', [
334334
}
335335
}).catch(function(error) {
336336
pgAdmin.Browser.notifier.pgRespErrorNotify(error);
337+
getApiInstance().delete(url_for('settings.delete_application_state'), {});
337338
});
338339
},
339340

web/pgadmin/settings/__init__.py

Lines changed: 76 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,11 @@
99

1010
"""Utility functions for storing and retrieving user configuration settings."""
1111
import os
12-
import traceback
1312
import json
1413

1514
from flask import Response, request, render_template, url_for, current_app
1615
from flask_babel import gettext
1716
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
2117

2218
from pgadmin.user_login_check import pga_login_required
2319
from pgadmin.utils import PgAdminModule
@@ -29,6 +25,10 @@
2925
from pgadmin.utils.constants import MIMETYPE_APP_JS
3026
from .utils import get_dialog_type, get_file_type_setting
3127
from cryptography.fernet import Fernet
28+
import hashlib
29+
from urllib.parse import unquote
30+
from pgadmin.utils.preferences import Preferences
31+
from pgadmin.utils import get_storage_directory
3232

3333
MODULE_NAME = 'settings'
3434

@@ -266,8 +266,6 @@ def get_file_format_setting():
266266
info=get_file_type_setting(list(data.values())))
267267

268268

269-
270-
271269
@blueprint.route(
272270
'/save_application_state',
273271
methods=["POST"], endpoint='save_application_state'
@@ -284,10 +282,12 @@ def save_application_state():
284282
tool_data = fernet.encrypt(json.dumps(data['tool_data']).encode())
285283
connection_info = data['connection_info'] \
286284
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
285+
if ('open_file_name' in connection_info and
286+
connection_info['open_file_name']):
287+
file_path = get_file_path(connection_info['open_file_name'],
288+
connection_info['storage'])
289+
connection_info['last_saved_file_hash'] = (
290+
get_last_saved_file_hash(file_path, trans_id))
291291

292292
try:
293293
data_entry = ApplicationState(
@@ -304,8 +304,28 @@ def save_application_state():
304304
data={
305305
'status': True,
306306
'msg': 'Success',
307-
}
308-
)
307+
})
308+
309+
310+
def get_last_saved_file_hash(file_path, trans_id):
311+
result = db.session \
312+
.query(ApplicationState) \
313+
.filter(ApplicationState.uid == current_user.id,
314+
ApplicationState.id == trans_id).all()
315+
file_hash_update_require = True
316+
last_saved_file_hash = None
317+
318+
for row in result:
319+
connection_info = row.connection_info
320+
if ('open_file_name' in connection_info and
321+
connection_info['open_file_name']):
322+
file_hash_update_require = not connection_info['is_editor_dirty']
323+
last_saved_file_hash = connection_info['last_saved_file_hash']
324+
325+
if file_hash_update_require:
326+
last_saved_file_hash = compute_sha256_large_file(file_path)
327+
328+
return last_saved_file_hash
309329

310330

311331
@blueprint.route(
@@ -326,22 +346,19 @@ def get_application_state():
326346
res = []
327347
for row in result:
328348
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']
349+
if ('open_file_name' in connection_info and
350+
connection_info['open_file_name']):
351+
file_path = get_file_path(
352+
connection_info['open_file_name'], connection_info['storage'])
332353
file_deleted = False if os.path.exists(file_path) else True
333354
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-
338355

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
356+
if (not file_deleted and connection_info['is_editor_dirty'] and
357+
'last_saved_file_hash' in connection_info and
358+
connection_info['last_saved_file_hash']):
359+
connection_info['external_file_changes'] = \
360+
check_external_file_changes(
361+
file_path, connection_info['last_saved_file_hash'])
345362

346363
res.append({'tool_name': row.tool_name,
347364
'connection_info': connection_info,
@@ -357,6 +374,31 @@ def get_application_state():
357374
)
358375

359376

377+
def get_file_path(file_name, storage):
378+
379+
file_path = unquote(file_name)
380+
381+
# get the current storage from request if available
382+
# or get it from last_storage preference.
383+
if storage:
384+
storage_folder = storage
385+
else:
386+
storage_folder = Preferences.module('file_manager').preference(
387+
'last_storage').get()
388+
389+
# retrieve storage directory path
390+
storage_manager_path = get_storage_directory(
391+
shared_storage=storage_folder)
392+
393+
if storage_manager_path:
394+
# generate full path of file
395+
file_path = os.path.join(
396+
storage_manager_path,
397+
file_path.lstrip('/').lstrip('\\')
398+
)
399+
return file_path
400+
401+
360402
@blueprint.route(
361403
'/delete_application_state/',
362404
methods=["DELETE"], endpoint='delete_application_state')
@@ -397,60 +439,33 @@ def delete_tool_data(trans_id=None):
397439
return False, str(e)
398440

399441

400-
import hashlib
401-
402-
def compute_sha256_large_data_in_memory(data):
442+
def compute_sha256_large_data_in_memory(data, chunk_size=8192):
403443
"""Hash large data (in-memory) by processing in chunks."""
404-
sha256_hash = hashlib.sha256()
444+
md5_hash = hashlib.md5()
405445
# Process data in 8 KB chunks
406446
string_data = json.loads(data)
407-
chunk_size = 8192
408447
for i in range(0, len(string_data), chunk_size):
409448
chunk = string_data[i:i + chunk_size]
410-
sha256_hash.update(chunk.encode("utf-8"))
449+
md5_hash.update(chunk.encode("utf-8"))
411450

412-
return sha256_hash.hexdigest()
451+
return md5_hash.hexdigest()
413452

414453

415-
def compute_sha256_large_file(file_path):
454+
def compute_sha256_large_file(file_path, chunk_size=8192):
416455
"""Compute SHA-256 hash for large files by reading in chunks."""
417-
sha256_hash = hashlib.sha256()
456+
md5_hash = hashlib.md5()
418457

419458
# Open the file in binary mode
420459
with open(file_path, "rb") as file:
421460
# 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()
461+
for chunk in iter(lambda: file.read(chunk_size), b""):
462+
md5_hash.update(chunk)
427463

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
464+
return md5_hash.hexdigest()
449465

450466

451467
def check_external_file_changes(file_path, last_saved_file_hash):
452468
current_file_hash = compute_sha256_large_file(file_path)
453469
if current_file_hash != last_saved_file_hash:
454470
return True
455471
return False
456-

web/pgadmin/settings/static/ApplicationStateProvider.jsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import getApiInstance from '../../static/js/api_instance';
1212
import url_for from 'sources/url_for';
1313
import { getBrowser } from '../../static/js/utils';
1414
import usePreferences from '../../preferences/static/js/store';
15+
import { usePgAdmin } from '../../static/js/PgAdminProvider';
1516

1617
const ApplicationStateContext = React.createContext();
1718

@@ -25,6 +26,7 @@ export function getToolData(localStorageId){
2526

2627
export function ApplicationStateProvider({children}){
2728
const preferencesStore = usePreferences();
29+
const pgAdmin = usePgAdmin();
2830
const saveAppState = preferencesStore?.getPreferencesForModule('misc')?.save_app_state;
2931
const openNewTab = preferencesStore?.getPreferencesForModule('browser')?.new_browser_tab_open;
3032

@@ -49,9 +51,21 @@ export function ApplicationStateProvider({children}){
4951
return saveAppState;
5052
};
5153

54+
const deleteToolData = (panelId, closePanelId) =>{
55+
if(panelId == closePanelId){
56+
let api = getApiInstance();
57+
api.delete(
58+
url_for('settings.delete_application_state'), {data:{'panelId': panelId}}
59+
).then(()=> { /* Sonar Qube */}).catch(function(error) {
60+
pgAdmin.Browser.notifier.pgRespErrorNotify(error);
61+
});
62+
}
63+
};
64+
5265
const value = useMemo(()=>({
5366
saveToolData,
54-
enableSaveToolData
67+
enableSaveToolData,
68+
deleteToolData
5569
}), []);
5670

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

web/pgadmin/static/js/ToolErrorView.jsx

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,20 @@ import PropTypes from 'prop-types';
22
import React from 'react';
33
import gettext from 'sources/gettext';
44
import { LAYOUT_EVENTS } from './helpers/Layout';
5-
import EmptyPanelMessage from './components/EmptyPanelMessage';
5+
import { styled } from '@mui/material/styles';
6+
import { Box } from '@mui/material';
7+
import { FormHelperText } from '@mui/material';
8+
import HTMLReactParse from 'html-react-parser';
9+
10+
const StyledBox = styled(Box)(({theme}) => ({
11+
color: theme.palette.text.primary,
12+
margin: '24px auto 12px',
13+
fontSize: '0.8rem',
14+
display: 'flex',
15+
alignItems: 'center',
16+
justifyContent: 'center',
17+
height: '100%',
18+
}));
619

720
export default function ToolErrorView({error, panelId, panelDocker}){
821

@@ -12,8 +25,10 @@ export default function ToolErrorView({error, panelId, panelDocker}){
1225
}
1326
});
1427

15-
let err_msg = gettext(`Unable to restore data due to error: ${error}`);
16-
return <EmptyPanelMessage error={gettext(err_msg)}/>;
28+
let err_msg = gettext(`There was some error while opening: ${error}`);
29+
return (<StyledBox>
30+
<FormHelperText variant="outlined" error= {true} style={{marginLeft: '4px'}} >{HTMLReactParse(err_msg)}</FormHelperText>
31+
</StyledBox>);
1732
}
1833

1934
ToolErrorView.propTypes = {

web/pgadmin/static/js/ToolView.jsx

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ import { BROWSER_PANELS } from '../../browser/static/js/constants';
1414
import PropTypes from 'prop-types';
1515
import LayoutIframeTab from './helpers/Layout/LayoutIframeTab';
1616
import { LAYOUT_EVENTS } from './helpers/Layout';
17-
import getApiInstance from './api_instance';
18-
import url_for from 'sources/url_for';
17+
import { useApplicationState } from '../../settings/static/ApplicationStateProvider';
18+
1919

2020
function ToolForm({actionUrl, params}) {
2121
const formRef = useRef(null);
@@ -40,6 +40,7 @@ ToolForm.propTypes = {
4040

4141
export default function ToolView({dockerObj}) {
4242
const pgAdmin = usePgAdmin();
43+
const {deleteToolData} = useApplicationState();
4344

4445
useEffect(()=>{
4546
pgAdmin.Browser.Events.on('pgadmin:tool:show', (panelId, toolUrl, formParams, tabParams, newTab)=>{
@@ -60,15 +61,8 @@ export default function ToolView({dockerObj}) {
6061
// case of workspace layout.
6162
let handler = pgAdmin.Browser.getDockerHandler?.(panelId, dockerObj);
6263
const deregisterRemove = handler.docker.eventBus.registerListener(LAYOUT_EVENTS.REMOVE, (closePanelId)=>{
63-
if(panelId == closePanelId){
64-
let api = getApiInstance();
65-
api.delete(
66-
url_for('settings.delete_application_state'), {data:{'panelId': panelId}}
67-
).then(()=> { /* Sonar Qube */}).catch(function(error) {
68-
pgAdmin.Browser.notifier.pgRespErrorNotify(error);
69-
});
70-
deregisterRemove();
71-
}
64+
deleteToolData(panelId, closePanelId);
65+
deregisterRemove();
7266
});
7367

7468
handler.focus();

web/pgadmin/static/js/components/EmptyPanelMessage.jsx

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@ import { styled } from '@mui/material/styles';
1212
import { Box } from '@mui/material';
1313
import InfoRoundedIcon from '@mui/icons-material/InfoRounded';
1414
import PropTypes from 'prop-types';
15-
import { FormHelperText } from '@mui/material';
16-
import HTMLReactParse from 'html-react-parser';
1715

1816
const StyledBox = styled(Box)(({theme}) => ({
1917
color: theme.palette.text.primary,
@@ -25,20 +23,16 @@ const StyledBox = styled(Box)(({theme}) => ({
2523
height: '100%',
2624
}));
2725

28-
export default function EmptyPanelMessage({text, style, error}) {
26+
export default function EmptyPanelMessage({text, style}) {
2927

3028
return (
3129
<StyledBox style={style}>
32-
{ error ? <FormHelperText variant="outlined" error= {true} style={{marginLeft: '4px'}} >{HTMLReactParse(error)}</FormHelperText> :
33-
<div>
34-
<InfoRoundedIcon style={{height: '1.2rem'}}/>
35-
<span style={{marginLeft: '4px'}}>{text}</span>
36-
</div>}
30+
<InfoRoundedIcon style={{height: '1.2rem'}}/>
31+
<span style={{marginLeft: '4px'}}>{text}</span>
3732
</StyledBox>
3833
);
3934
}
4035
EmptyPanelMessage.propTypes = {
4136
text: PropTypes.string,
4237
style: PropTypes.object,
43-
error: PropTypes.string,
4438
};

web/pgadmin/static/js/helpers/Layout/index.jsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import usePreferences from '../../../../preferences/static/js/store';
2626
import _ from 'lodash';
2727
import UtilityView from '../../UtilityView';
2828
import ToolView from '../../ToolView';
29+
import { ApplicationStateProvider } from '../../../../settings/static/ApplicationStateProvider';
2930

3031
function TabTitle({id, closable, defaultInternal}) {
3132
const layoutDocker = React.useContext(LayoutDockerContext);
@@ -497,7 +498,9 @@ export default function Layout({groups, noContextGroups, getLayoutInstance, layo
497498
label="Layout Context Menu" />
498499
{enableToolEvents && <>
499500
<UtilityView dockerObj={layoutDockerObj} />
500-
<ToolView dockerObj={layoutDockerObj} />
501+
<ApplicationStateProvider>
502+
<ToolView dockerObj={layoutDockerObj} />
503+
</ApplicationStateProvider>
501504
</>}
502505
</LayoutDockerContext.Provider>
503506
);

0 commit comments

Comments
 (0)