Skip to content

Commit

Permalink
Allow users to convert View/Edit table into a Query tool to enable ed…
Browse files Browse the repository at this point in the history
…iting the SQL generated. pgadmin-org#5908
  • Loading branch information
nikhil-mohite authored Dec 19, 2023
1 parent 4db13fa commit 0458065
Show file tree
Hide file tree
Showing 18 changed files with 296 additions and 20 deletions.
15 changes: 15 additions & 0 deletions docs/en_US/editgrid.rst
Original file line number Diff line number Diff line change
Expand Up @@ -222,3 +222,18 @@ To delete a row from the grid, click the trash icon.
:maxdepth: 2

viewdata_filter



Promote View/Edit Data to Query Tool
************************************

A View/Edit Data tab can be converted to a Query Tool Tab just by editing the query. Once you start editing, it will ask if you really want to move away from View/Edit.

.. image:: images/promote_view_edit_data_warning.png
:alt: Promote View/Edit Data tab to Query Tool tab warning
:align: center

You can disable the dialog by selecting the "Don't Ask again" checkbox. If you wish to resume the confirmation dialog, you can do it from "Prefrences -> Query Tool -> Editor -> Show View/Edit Data Promotion Warning?"

Once you chose to continue, you won't be able to use the features of View/Edit mode like the filter and sorting options, limit, etc. It is a one-way conversion. It will be a query tool now.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
33 changes: 31 additions & 2 deletions web/pgadmin/preferences/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from pgadmin.utils import PgAdminModule
from pgadmin.utils.ajax import success_return, \
make_response as ajax_response, internal_server_error
from pgadmin.utils.menu import MenuItem
from pgadmin.utils.ajax import make_json_response
from pgadmin.utils.preferences import Preferences
from pgadmin.utils.constants import MIMETYPE_APP_JS
from pgadmin.browser.server_groups import ServerGroupModule as sgm
Expand All @@ -47,7 +47,9 @@ def get_exposed_url_endpoints(self):
return [
'preferences.index',
'preferences.get_by_name',
'preferences.get_all'
'preferences.get_all',
'preferences.update_pref'

]


Expand Down Expand Up @@ -245,3 +247,30 @@ def save():
**domain)

return response


@blueprint.route("/update", methods=["PUT"], endpoint="update_pref")
@login_required
def update():
"""
Update a specific preference.
"""
pref_data = get_data()
pref_data = json.loads(pref_data['pref_data'])

for data in pref_data:
if data['name'] in ['vw_edt_tab_title_placeholder',
'qt_tab_title_placeholder',
'debugger_tab_title_placeholder'] \
and data['value'].isspace():
data['value'] = ''

pref_module = Preferences.module(data['module'])
pref = pref_module.preference(data['name'])
# set user preferences
pref.set(data['value'])

return make_json_response(
data={'data': 'Success'},
status=200
)
9 changes: 9 additions & 0 deletions web/pgadmin/preferences/static/js/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ const usePreferences = create((set, get)=>({
get().data, {'module': module, 'name': preference}
);
},
setPreference: (data)=> {
// Update Preferences and then refresh cache.
getApiInstance().put(url_for('preferences.update_pref'), data).then(()=> {
preferenceChangeBroadcast.postMessage('refresh');
});
},
getPreferencesForModule: function(module) {
let preferences = {};
_.forEach(
Expand Down Expand Up @@ -62,6 +68,9 @@ export function setupPreferenceBroadcast() {
if(ev.data == 'sync') {
broadcast(usePreferences.getState());
}
if(ev.data == 'refresh') {
usePreferences.getState().cache();
}
};
}

Expand Down
12 changes: 12 additions & 0 deletions web/pgadmin/preferences/tests/preferences_test_data.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,22 @@
"url": "/preferences/",
"is_positive_test": true,
"mocking_required": false,
"update_spec_pref": false,
"mock_data": {},
"expected_data": {
"status_code": 200
}
},{
"name": "Update specific preference",
"url": "/preferences/update_pref",
"is_positive_test": true,
"mocking_required": false,
"mock_data": {},
"update_spec_pref": true,
"expected_data": {
"status_code": 200
}
}

]
}
14 changes: 13 additions & 1 deletion web/pgadmin/preferences/tests/test_preferences_update.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,10 @@ def setUp(self):
parent_node_dict['preferences'] = response.data

def runTest(self):
self.update_preferences()
if self.update_spec_pref:
self.update_preference()
else:
self.update_preferences()

def update_preferences(self):
if 'preferences' in parent_node_dict:
Expand All @@ -58,3 +61,12 @@ def update_preferences(self):
self.assertTrue(response.status_code, 200)
else:
self.fail('Preferences not found')

def update_preference(self):
updated_data = [{'name': 'view_edit_promotion_warning',
'value': False,
'module': 'sqleditor'}]
response = self.tester.put(self.url,
data=json.dumps(updated_data),
content_type='html/json')
self.assertTrue(response.status_code, 200)
2 changes: 1 addition & 1 deletion web/pgadmin/static/js/helpers/ModalProvider.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ function ModalContainer({ id, title, content, dialogHeight, dialogWidth, onClose
return;
}
useModalRef.closeModal(id);
if(reason == 'escapeKeyDown') {
if(reason == 'escapeKeyDown' || reason == undefined) {
onClose?.();
}
};
Expand Down
26 changes: 24 additions & 2 deletions web/pgadmin/tools/sqleditor/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -415,7 +415,11 @@ def _connect(conn, **kwargs):

def _init_sqleditor(trans_id, connect, sgid, sid, did, dbname=None, **kwargs):
# Create asynchronous connection using random connection id.
conn_id = str(secrets.choice(range(1, 9999999)))
conn_id = kwargs['conn_id'] if 'conn_id' in kwargs else str(
secrets.choice(range(1, 9999999)))
if 'conn_id' in kwargs:
kwargs.pop('conn_id')

conn_id_ac = str(secrets.choice(range(1, 9999999)))

manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(sid)
Expand All @@ -425,7 +429,7 @@ def _init_sqleditor(trans_id, connect, sgid, sid, did, dbname=None, **kwargs):
try:
command_obj = ObjectRegistry.get_object(
'query_tool', conn_id=conn_id, sgid=sgid, sid=sid, did=did,
conn_id_ac=conn_id_ac
conn_id_ac=conn_id_ac, **kwargs
)
except Exception as e:
current_app.logger.error(e)
Expand Down Expand Up @@ -868,6 +872,24 @@ def start_query_tool(trans_id):
)

connect = 'connect' in request.args and request.args['connect'] == '1'
if 'gridData' in session and str(trans_id) in session['gridData']:
data = pickle.loads(session['gridData'][str(trans_id)]['command_obj'])
if data.object_type == 'table':
manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(
data.sid)
default_conn = manager.connection(conn_id=data.conn_id,
did=data.did)
kwargs = {
'user': default_conn.manager.user,
'role': default_conn.manager.role,
'password': default_conn.manager.password,
'conn_id': data.conn_id
}
is_error, errmsg, conn_id, version = _init_sqleditor(
trans_id, connect, data.sgid, data.sid, data.did, **kwargs)

if is_error:
return errmsg

return StartRunningQuery(blueprint, current_app.logger).execute(
sql, trans_id, session, connect
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ export default function QueryToolComponent({params, pgWindow, pgAdmin, selectedN
title: _.unescape(params.title),
is_query_tool: params.is_query_tool == 'true' ? true : false,
node_name: retrieveNodeName(selectedNodeInfo),
dbname: _.unescape(params.database_name) || getDatabaseLabel(selectedNodeInfo)
},
connection_list: [{
sgid: params.sgid,
Expand Down Expand Up @@ -746,7 +747,37 @@ export default function QueryToolComponent({params, pgWindow, pgAdmin, selectedN
modal: modal,
params: qtState.params,
preferences: qtState.preferences,
mainContainerRef: containerRef
mainContainerRef: containerRef,
toggleQueryTool: () => setQtState((prev)=>{
return {
...prev,
params: {
...prev.params,
is_query_tool: true
}
};
}),
updateTitle: (title) => {
setPanelTitle(qtPanelDocker, qtPanelId, title, qtState, isDirtyRef.current);
setQtState((prev) => {
// Update connection Title
let newConnList = [...prev.connection_list];
newConnList.forEach((conn) => {
if (conn.sgid == params.sgid && conn.sid == params.sid && conn.did == params.did) {
conn.title = title;
conn.conn_title = title;
}
});
return {
...prev,
params: {
...prev.params,
title: title
},
connection_list: newConnList,
};
});
},
}), [qtState.params, qtState.preferences, containerRef.current]);

const queryToolConnContextValue = React.useMemo(()=>({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export const QUERY_TOOL_EVENTS = {

COPY_DATA: 'COPY_DATA',
SET_LIMIT_VALUE: 'SET_LIMIT_VALUE',
PROMOTE_TO_QUERY_TOOL: 'PROMOTE_TO_QUERY_TOOL',
SET_CONNECTION_STATUS: 'SET_CONNECTION_STATUS',
EXECUTION_START: 'EXECUTION_START',
EXECUTION_END: 'EXECUTION_END',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import React, { useState } from 'react';
import { useModalStyles } from '../../../../../../static/js/helpers/ModalProvider';
import gettext from 'sources/gettext';
import { Box, makeStyles } from '@material-ui/core';
import { DefaultButton, PrimaryButton } from '../../../../../../static/js/components/Buttons';
import CloseIcon from '@material-ui/icons/CloseRounded';
import HTMLReactParser from 'html-react-parser';
import PropTypes from 'prop-types';
import CheckRounded from '@material-ui/icons/CheckRounded';
import { InputCheckbox } from '../../../../../../static/js/components/FormComponents';


const useStyles = makeStyles(() => ({
saveChoice: {
margin: '10px 0 10px 10px',
}
}));


export default function ConfirmPromotionContent({ onContinue, onClose, closeModal, text }) {
const [formData, setFormData] = useState({
save_user_choice: false
});

const onDataChange = (e, id) => {
let val = e;
if (e?.target) {
val = e.target.value;
}
setFormData((prev) => ({ ...prev, [id]: val }));
};
const modalClasses = useModalStyles();
const classes = useStyles();

return (
<Box display="flex" flexDirection="column" height="100%">
<Box flexGrow="1" p={2}>{typeof (text) == 'string' ? HTMLReactParser(text) : text}</Box>
<Box className={classes.saveChoice}>
<InputCheckbox controlProps={{ label: gettext('Don\'t ask again') }} value={formData['save_user_choice']}
onChange={(e) => onDataChange(e.target.checked, 'save_user_choice')} />
</Box>
<Box className={modalClasses.footer}>
<DefaultButton data-test="close" startIcon={<CloseIcon />} onClick={() => {
onClose?.();
closeModal();
}} >{gettext('Cancel')}</DefaultButton>
<PrimaryButton data-test="Continue" className={modalClasses.margin} startIcon={<CheckRounded />} onClick={() => {
let postFormData = new FormData();
postFormData.append('pref_data', JSON.stringify([{ 'name': 'view_edit_promotion_warning', 'value': !formData.save_user_choice, 'module': 'sqleditor' }]));
onContinue?.(postFormData);
closeModal();
}} autoFocus={true} >{gettext('Continue')}</PrimaryButton>
</Box>
</Box>
);
}

ConfirmPromotionContent.propTypes = {
closeModal: PropTypes.func,
text: PropTypes.string,
onContinue: PropTypes.func,
onClose: PropTypes.func
};
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,16 @@ export function MainToolBar({containerRef, onFilterClick, onManageMacros}) {
eventBus.registerListener(QUERY_TOOL_EVENTS.SET_LIMIT_VALUE, (l)=>{
setLimit(l);
});

eventBus.registerListener(QUERY_TOOL_EVENTS.PROMOTE_TO_QUERY_TOOL, ()=>{
setDisableButton('filter', true);
setDisableButton('limit', true);

setDisableButton('execute', false);
setDisableButton('execute-options', false);
});


}, []);

useEffect(()=>{
Expand Down
Loading

0 comments on commit 0458065

Please sign in to comment.