Replace infinite scrolling with pagination in query tool data output for better UX and performance. #1780

pull/7990/head
Aditya Toshniwal 2024-10-01 14:47:12 +05:30 committed by GitHub
parent f8fb78be11
commit 6322674d98
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 554 additions and 351 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 183 KiB

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 151 KiB

View File

@ -230,7 +230,7 @@ Use the fields on the *Options* panel to manage ERD preferences.
* Use *Cardinality Notation* to change the cardinality notation format * Use *Cardinality Notation* to change the cardinality notation format
used to present relationship links. used to present relationship links.
* When the *SQL With DROP Table* switch is set to *True*, the SQL * When the *SQL With DROP Table* switch is set to *True*, the SQL
generated by the ERD Tool will add DROP table DDL before each CREATE generated by the ERD Tool will add DROP table DDL before each CREATE
@ -398,7 +398,7 @@ Use the fields on the *Editor* panel to change settings of the query editor.
changed to text/plain. Keyword highlighting and code folding will be disabled. changed to text/plain. Keyword highlighting and code folding will be disabled.
This will improve editor performance with large files. This will improve editor performance with large files.
* When the *Highlight selection matches?* switch is set to *True*, the editor will * When the *Highlight selection matches?* switch is set to *True*, the editor will
highlight matched selected text. highlight matched selected text.
.. image:: images/preferences_sql_explain.png .. image:: images/preferences_sql_explain.png
@ -476,11 +476,11 @@ Use the fields on the *Options* panel to manage editor preferences.
* When the *Show View/Edit Data Promotion Warning?* switch is set to *True* * When the *Show View/Edit Data Promotion Warning?* switch is set to *True*
View/Edit Data tool will show promote to Query tool confirm dialog on query edit. View/Edit Data tool will show promote to Query tool confirm dialog on query edit.
* When the *Underline query at cursor?* switch is set to *True*, query tool will * When the *Underline query at cursor?* switch is set to *True*, query tool will
parse and underline the query at the cursor position. parse and underline the query at the cursor position.
* When the *Underlined query execute warning?* switch is set to *True*, query tool * When the *Underlined query execute warning?* switch is set to *True*, query tool
will warn upon clicking the *Execute Query* button in the query tool. The warning will warn upon clicking the *Execute Query* button in the query tool. The warning
will appear only if *Underline query at cursor?* is set to *False*. will appear only if *Underline query at cursor?* is set to *False*.
.. image:: images/preferences_sql_results_grid.png .. image:: images/preferences_sql_results_grid.png
@ -497,9 +497,8 @@ preferences for copied data.
* Specify the maximum width of the column in pixels when 'Columns sized by' is * Specify the maximum width of the column in pixels when 'Columns sized by' is
set to *Column data*. If 'Columns sized by' is set to *Column name* then this set to *Column data*. If 'Columns sized by' is set to *Column name* then this
setting won't have any effect. setting won't have any effect.
* Specify the number of records to fetch in one batch in query tool when * Specify the number of records to fetch in one batch. Changing this value will
query result set is large. Changing this value will override override DATA_RESULT_ROWS_PER_PAGE setting from config file.
ON_DEMAND_ROW_COUNT setting from config file.
* Use the *Result copy field separator* drop-down listbox to select the field * Use the *Result copy field separator* drop-down listbox to select the field
separator for copied data. separator for copied data.
* Use the *Result copy quote character* drop-down listbox to select the quote * Use the *Result copy quote character* drop-down listbox to select the quote
@ -523,7 +522,7 @@ reformatting of SQL.
* Use the *Data type case* option to specify whether to change data types * Use the *Data type case* option to specify whether to change data types
into upper, lower, or preserve case. into upper, lower, or preserve case.
* Use the *Expression width* option to specify maximum number of characters * Use the *Expression width* option to specify maximum number of characters
in parenthesized expressions to be kept on single line. in parenthesized expressions to be kept on single line.
* Use the *Function case* option to specify whether to change function * Use the *Function case* option to specify whether to change function
names into upper, lower, or preserve case. names into upper, lower, or preserve case.
@ -531,7 +530,7 @@ reformatting of SQL.
(object names) into upper, lower, or capitalized case. (object names) into upper, lower, or capitalized case.
* Use the *Keyword case* option to specify whether to change keywords into * Use the *Keyword case* option to specify whether to change keywords into
upper, lower, or preserve case. upper, lower, or preserve case.
* Use *Lines between queries* to specify how many empty lines to leave * Use *Lines between queries* to specify how many empty lines to leave
between SQL statements. If set to zero it puts no new line. between SQL statements. If set to zero it puts no new line.
* Use *Logical operator new line* to specify newline placement before or * Use *Logical operator new line* to specify newline placement before or
after logical operators (AND, OR, XOR). after logical operators (AND, OR, XOR).

View File

@ -74,8 +74,8 @@ key combination to select from a popup menu of autocomplete options.
After entering a query, select the *Execute script* icon from the toolbar. The After entering a query, select the *Execute script* icon from the toolbar. The
complete contents of the SQL editor panel will be sent to the database server complete contents of the SQL editor panel will be sent to the database server
for execution. To execute only a section of the code that is displayed in the for execution. To execute only a section of the code that is displayed in the
SQL editor, highlight the text that you want the server to execute, and click the SQL editor, highlight the text that you want the server to execute, and click the
*Execute script* icon. *Execute script* icon.
.. image:: images/query_execute_script.png .. image:: images/query_execute_script.png
@ -177,6 +177,7 @@ You can:
* Use the *Save results to file* icon to save the content of the *Data Output* * Use the *Save results to file* icon to save the content of the *Data Output*
tab as a comma-delimited file. tab as a comma-delimited file.
* Edit the data in the result set of a SELECT query if it is updatable. * Edit the data in the result set of a SELECT query if it is updatable.
* Move between pages of data result.
.. _updatable-result-set: .. _updatable-result-set:

View File

@ -214,6 +214,48 @@ Data Editing Options
| SQL | Use the SQL button to check the current query that gave the data. | | | SQL | Use the SQL button to check the current query that gave the data. | |
+----------------------+---------------------------------------------------------------------------------------------------+----------------+ +----------------------+---------------------------------------------------------------------------------------------------+----------------+
Pagination Options
********************
.. image:: images/query_data_pagination.png
:alt: Query tool data pagination options
:align: center
.. table::
:class: longtable
:widths: 1 4 1
+----------------------+---------------------------------------------------------------------------------------------------+----------------+
| Icon | Behavior | Shortcut |
+======================+===================================================================================================+================+
| *Rows Range* | Show the current row numbers visible in the data grid. | |
+----------------------+---------------------------------------------------------------------------------------------------+----------------+
| *Edit Range* | Click to open the from and to rows range inputs to allow setting them. | |
+----------------------+---------------------------------------------------------------------------------------------------+----------------+
| *Page No* | Enter the page no you want to jump to out of total shown next to this input | |
+----------------------+---------------------------------------------------------------------------------------------------+----------------+
| *First Page* | Click to go to the first page. | |
+----------------------+---------------------------------------------------------------------------------------------------+----------------+
| *Previous Page* | Click to go to the previous page. | |
+----------------------+---------------------------------------------------------------------------------------------------+----------------+
| *Next Page* | Click to go to the next page. | |
+----------------------+---------------------------------------------------------------------------------------------------+----------------+
| *Last Page* | Click to go to the last page. | |
+----------------------+---------------------------------------------------------------------------------------------------+----------------+
.. image:: images/query_data_pagination_edit.png
:alt: Query tool data pagination options
:align: center
One can click the edit range button to open rows range editor:
* From and to range should be between 1 and total rows.
* The range can be applied by clicking the *Apply* button or by pressing enter in the range inputs.
* Once the range is applied, pgAdmin will recalculate the rows per page. The pagination will then behave based on the new rows per page.
* It may be possible that on pressing next page button, the new rows range is not next to manually enterred range.
Status Bar Status Bar
********** **********

View File

@ -513,10 +513,10 @@ THREADED_MODE = True
SQLALCHEMY_TRACK_MODIFICATIONS = False SQLALCHEMY_TRACK_MODIFICATIONS = False
########################################################################## ##########################################################################
# Number of records to fetch in one batch in query tool when query result # Number of records to fetch in one page in query tool when query result
# set is large. # set is large and is divided in multiple pages
########################################################################## ##########################################################################
ON_DEMAND_RECORD_COUNT = 1000 DATA_RESULT_ROWS_PER_PAGE = 1000
########################################################################## ##########################################################################
# Allow users to display Gravatar image for their username in Server mode # Allow users to display Gravatar image for their username in Server mode

View File

@ -15,6 +15,7 @@ import secrets
from urllib.parse import unquote from urllib.parse import unquote
from threading import Lock from threading import Lock
import threading import threading
import math
import json import json
from config import PG_DEFAULT_DRIVER, ALLOW_SAVE_PASSWORD, SHARED_STORAGE from config import PG_DEFAULT_DRIVER, ALLOW_SAVE_PASSWORD, SHARED_STORAGE
@ -106,8 +107,7 @@ class SqlEditorModule(PgAdminModule):
'sqleditor.view_data_start', 'sqleditor.view_data_start',
'sqleditor.query_tool_start', 'sqleditor.query_tool_start',
'sqleditor.poll', 'sqleditor.poll',
'sqleditor.fetch', 'sqleditor.fetch_window',
'sqleditor.fetch_all',
'sqleditor.fetch_all_from_start', 'sqleditor.fetch_all_from_start',
'sqleditor.save', 'sqleditor.save',
'sqleditor.inclusive_filter', 'sqleditor.inclusive_filter',
@ -470,7 +470,8 @@ def _init_sqleditor(trans_id, connect, sgid, sid, did, dbname=None, **kwargs):
"prompt_password": True, "prompt_password": True,
"allow_save_password": True "allow_save_password": True
if ALLOW_SAVE_PASSWORD and if ALLOW_SAVE_PASSWORD and
session['allow_save_password'] else False, session.get('allow_save_password', None)
else False,
} }
), '', '' ), '', ''
else: else:
@ -925,7 +926,6 @@ def poll(trans_id):
rows_affected = 0 rows_affected = 0
rows_fetched_from = 0 rows_fetched_from = 0
rows_fetched_to = 0 rows_fetched_to = 0
has_more_rows = False
columns = dict() columns = dict()
columns_info = None columns_info = None
primary_keys = None primary_keys = None
@ -936,8 +936,8 @@ def poll(trans_id):
additional_messages = None additional_messages = None
notifies = None notifies = None
data_obj = {} data_obj = {}
on_demand_record_count = Preferences.module(MODULE_NAME).\ data_result_rows_per_page = Preferences.module(MODULE_NAME).\
preference('on_demand_record_count').get() preference('data_result_rows_per_page').get()
# Check the transaction and connection status # Check the transaction and connection status
status, error_msg, conn, trans_obj, session_obj = \ status, error_msg, conn, trans_obj, session_obj = \
check_transaction_status(trans_id) check_transaction_status(trans_id)
@ -1004,7 +1004,8 @@ def poll(trans_id):
status = 'Success' status = 'Success'
rows_affected = conn.rows_affected() rows_affected = conn.rows_affected()
st, result = conn.async_fetchmany_2darray(on_demand_record_count) st, result = \
conn.async_fetchmany_2darray(data_result_rows_per_page)
# There may be additional messages even if result is present # There may be additional messages even if result is present
# eg: Function can provide result as well as RAISE messages # eg: Function can provide result as well as RAISE messages
@ -1081,8 +1082,6 @@ def poll(trans_id):
# means nothing to fetch # means nothing to fetch
if result and rows_affected > -1: if result and rows_affected > -1:
res_len = len(result) res_len = len(result)
if res_len == on_demand_record_count:
has_more_rows = True
if res_len > 0: if res_len > 0:
rows_fetched_from = trans_obj.get_fetched_row_cnt() rows_fetched_from = trans_obj.get_fetched_row_cnt()
@ -1126,6 +1125,15 @@ def poll(trans_id):
data_obj['db_id'] = trans_obj.did \ data_obj['db_id'] = trans_obj.did \
if trans_obj is not None and hasattr(trans_obj, 'did') else 0 if trans_obj is not None and hasattr(trans_obj, 'did') else 0
page_size = rows_fetched_to - rows_fetched_from + 1
pagination = {
'page_size': page_size,
'page_count': math.ceil(conn.total_rows / page_size),
'page_no': math.floor(rows_fetched_from / page_size) + 1,
'rows_from': rows_fetched_from,
'rows_to': rows_fetched_to
}
return make_json_response( return make_json_response(
data={ data={
'status': status, 'result': result, 'status': status, 'result': result,
@ -1134,7 +1142,6 @@ def poll(trans_id):
'rows_fetched_to': rows_fetched_to, 'rows_fetched_to': rows_fetched_to,
'additional_messages': additional_messages, 'additional_messages': additional_messages,
'notifies': notifies, 'notifies': notifies,
'has_more_rows': has_more_rows,
'colinfo': columns_info, 'colinfo': columns_info,
'primary_keys': primary_keys, 'primary_keys': primary_keys,
'types': types, 'types': types,
@ -1143,26 +1150,20 @@ def poll(trans_id):
'oids': oids, 'oids': oids,
'transaction_status': transaction_status, 'transaction_status': transaction_status,
'data_obj': data_obj, 'data_obj': data_obj,
'pagination': pagination,
} }
) )
@blueprint.route( @blueprint.route(
'/fetch/<int:trans_id>', methods=["GET"], endpoint='fetch' '/fetch_window/<int:trans_id>/<int:from_rownum>/<int:to_rownum>',
) methods=["GET"], endpoint='fetch_window'
@blueprint.route(
'/fetch/<int:trans_id>/<int:fetch_all>', methods=["GET"],
endpoint='fetch_all'
) )
@pga_login_required @pga_login_required
def fetch(trans_id, fetch_all=None): def fetch_window(trans_id, from_rownum=0, to_rownum=0):
result = None result = None
has_more_rows = False
rows_fetched_from = 0 rows_fetched_from = 0
rows_fetched_to = 0 rows_fetched_to = 0
on_demand_record_count = Preferences.module(MODULE_NAME).preference(
'on_demand_record_count').get()
fetch_row_cnt = -1 if fetch_all == 1 else on_demand_record_count
# Check the transaction and connection status # Check the transaction and connection status
status, error_msg, conn, trans_obj, session_obj = \ status, error_msg, conn, trans_obj, session_obj = \
@ -1174,33 +1175,39 @@ def fetch(trans_id, fetch_all=None):
status=404) status=404)
if status and conn is not None and session_obj is not None: if status and conn is not None and session_obj is not None:
status, result = conn.async_fetchmany_2darray(fetch_row_cnt) # rownums start from 0 but UI will ask from 1
status, result = conn.async_fetchmany_2darray(
records=None, from_rownum=from_rownum - 1, to_rownum=to_rownum - 1)
if not status: if not status:
status = 'Error' status = 'Error'
else: else:
status = 'Success' status = 'Success'
res_len = len(result) if result else 0 res_len = len(result) if result else 0
if fetch_row_cnt != -1 and res_len == on_demand_record_count:
has_more_rows = True
if res_len: if res_len:
rows_fetched_from = trans_obj.get_fetched_row_cnt() rows_fetched_from = from_rownum
trans_obj.update_fetched_row_cnt(rows_fetched_from + res_len) rows_fetched_to = rows_fetched_from + res_len - 1
rows_fetched_from += 1
rows_fetched_to = trans_obj.get_fetched_row_cnt()
session_obj['command_obj'] = pickle.dumps(trans_obj, -1) session_obj['command_obj'] = pickle.dumps(trans_obj, -1)
update_session_grid_transaction(trans_id, session_obj) update_session_grid_transaction(trans_id, session_obj)
else: else:
status = 'NotConnected' status = 'NotConnected'
result = error_msg result = error_msg
page_size = to_rownum - from_rownum + 1
pagination = {
'page_size': page_size,
'page_count': math.ceil(conn.total_rows / page_size),
'page_no': math.floor(rows_fetched_from / page_size) + 1,
'rows_from': rows_fetched_from,
'rows_to': rows_fetched_to
}
return make_json_response( return make_json_response(
data={ data={
'status': status, 'status': status,
'result': result, 'result': result,
'has_more_rows': has_more_rows, 'pagination': pagination,
'rows_fetched_from': rows_fetched_from, 'row_count': conn.row_count,
'rows_fetched_to': rows_fetched_to
} }
) )

View File

@ -37,7 +37,7 @@ export const QUERY_TOOL_EVENTS = {
STOP_QUERY: 'STOP_QUERY', STOP_QUERY: 'STOP_QUERY',
CURSOR_ACTIVITY: 'CURSOR_ACTIVITY', CURSOR_ACTIVITY: 'CURSOR_ACTIVITY',
SET_MESSAGE: 'SET_MESSAGE', SET_MESSAGE: 'SET_MESSAGE',
ROWS_FETCHED: 'ROWS_FETCHED', TOTAL_ROWS_COUNT: 'TOTAL_ROWS_COUNT',
SELECTED_ROWS_COLS_CELL_CHANGED: 'SELECTED_ROWS_COLS_CELL_CHANGED', SELECTED_ROWS_COLS_CELL_CHANGED: 'SELECTED_ROWS_COLS_CELL_CHANGED',
DATAGRID_CHANGED: 'DATAGRID_CHANGED', DATAGRID_CHANGED: 'DATAGRID_CHANGED',
HIGHLIGHT_ERROR: 'HIGHLIGHT_ERROR', HIGHLIGHT_ERROR: 'HIGHLIGHT_ERROR',
@ -56,8 +56,12 @@ export const QUERY_TOOL_EVENTS = {
PUSH_HISTORY: 'PUSH_HISTORY', PUSH_HISTORY: 'PUSH_HISTORY',
HANDLE_API_ERROR: 'HANDLE_API_ERROR', HANDLE_API_ERROR: 'HANDLE_API_ERROR',
SET_FILTER_INFO: 'SET_FILTER_INFO', SET_FILTER_INFO: 'SET_FILTER_INFO',
FETCH_MORE_ROWS: 'FETCH_MORE_ROWS',
REINIT_QT_CONNECTION:'REINIT_QT_CONNECTION', REINIT_QT_CONNECTION:'REINIT_QT_CONNECTION',
FETCH_WINDOW: 'FETCH_WINDOW',
ALL_PAGE_ROWS_SELECTED: 'ALL_PAGE_ROWS_SELECTED',
ALL_ROWS_SELECTED: 'ALL_ROWS_SELECTED',
CLEAR_ROWS_SELECTED: 'CLEAR_ROWS_SELECTED',
ALL_ROWS_SELECTED_STATUS: 'ALL_ROWS_SELECTED_STATUS',
EDITOR_LAST_FOCUS: 'EDITOR_LAST_FOCUS', EDITOR_LAST_FOCUS: 'EDITOR_LAST_FOCUS',
EDITOR_FIND_REPLACE: 'EDITOR_FIND_REPLACE', EDITOR_FIND_REPLACE: 'EDITOR_FIND_REPLACE',
@ -109,4 +113,4 @@ export const PANELS = {
export const MAX_QUERY_LENGTH = 1000000; export const MAX_QUERY_LENGTH = 1000000;
export const OS_EOL = navigator.platform === 'win32' ? 'crlf' : 'lf'; export const OS_EOL = navigator.platform === 'win32' ? 'crlf' : 'lf';

View File

@ -139,9 +139,8 @@ function SelectAllHeaderRenderer({isCellSelected}) {
const eventBus = useContext(QueryToolEventsContext); const eventBus = useContext(QueryToolEventsContext);
const dataGridExtras = useContext(DataGridExtrasContext); const dataGridExtras = useContext(DataGridExtrasContext);
const onClick = ()=>{ const onClick = ()=>{
eventBus.fireEvent(QUERY_TOOL_EVENTS.FETCH_MORE_ROWS, true, ()=>{ onRowSelectionChange({ type: 'HEADER', checked: !isRowSelected });
onRowSelectionChange({ type: 'HEADER', checked: !isRowSelected }); eventBus.fireEvent(QUERY_TOOL_EVENTS.ALL_PAGE_ROWS_SELECTED, !isRowSelected);
});
}; };
useLayoutEffect(() => { useLayoutEffect(() => {
@ -149,6 +148,15 @@ function SelectAllHeaderRenderer({isCellSelected}) {
cellRef.current?.focus({ preventScroll: true }); cellRef.current?.focus({ preventScroll: true });
}, [isCellSelected]); }, [isCellSelected]);
useEffect(()=>{
const unregClear = eventBus.registerListener(QUERY_TOOL_EVENTS.CLEAR_ROWS_SELECTED, ()=>{
onRowSelectionChange({ type: 'HEADER', checked: false });
});
return ()=>{
unregClear();
};
}, []);
return <div ref={cellRef} style={{width: '100%', height: '100%'}} onClick={onClick} return <div ref={cellRef} style={{width: '100%', height: '100%'}} onClick={onClick}
tabIndex="0" onKeyDown={getCopyShortcutHandler(dataGridExtras.handleCopy)}></div>; tabIndex="0" onKeyDown={getCopyShortcutHandler(dataGridExtras.handleCopy)}></div>;
} }
@ -167,15 +175,13 @@ function SelectableHeaderRenderer({column, selectedColumns, onSelectedColumnsCha
} }
const onClick = ()=>{ const onClick = ()=>{
eventBus.fireEvent(QUERY_TOOL_EVENTS.FETCH_MORE_ROWS, true, ()=>{ const newSelectedCols = new Set(selectedColumns);
const newSelectedCols = new Set(selectedColumns); if (newSelectedCols.has(column.idx)) {
if (newSelectedCols.has(column.idx)) { newSelectedCols.delete(column.idx);
newSelectedCols.delete(column.idx); } else {
} else { newSelectedCols.add(column.idx);
newSelectedCols.add(column.idx); }
} onSelectedColumnsChange(newSelectedCols);
onSelectedColumnsChange(newSelectedCols);
});
}; };
const isSelected = selectedColumns.has(column.idx); const isSelected = selectedColumns.has(column.idx);
@ -296,9 +302,10 @@ function initialiseColumns(columns, rows, totalRowCount, columnWidthBy) {
} }
function RowNumColFormatter({row, rowKeyGetter, rowIdx, dataChangeStore, onSelectedColumnsChange}) { function RowNumColFormatter({row, rowKeyGetter, rowIdx, dataChangeStore, onSelectedColumnsChange}) {
const [isRowSelected, onRowSelectionChange] = useRowSelection(); const [isRowSelected, onRowSelectionChange] = useRowSelection();
const {startRowNum} = useContext(DataGridExtrasContext);
let rowKey = rowKeyGetter(row); let rowKey = rowKeyGetter(row);
let rownum = rowIdx+1; let rownum = rowIdx+(startRowNum??1);
if(rowKey in (dataChangeStore?.added || {})) { if(rowKey in (dataChangeStore?.added || {})) {
rownum = rownum+'+'; rownum = rownum+'+';
} else if(rowKey in (dataChangeStore?.deleted || {})) { } else if(rowKey in (dataChangeStore?.deleted || {})) {
@ -375,7 +382,7 @@ function getTextWidth(column, rows, canvas, columnWidthBy) {
} }
export default function QueryToolDataGrid({columns, rows, totalRowCount, dataChangeStore, export default function QueryToolDataGrid({columns, rows, totalRowCount, dataChangeStore,
onSelectedCellChange, selectedColumns, onSelectedColumnsChange, columnWidthBy, ...props}) { onSelectedCellChange, selectedColumns, onSelectedColumnsChange, columnWidthBy, startRowNum, ...props}) {
const [readyColumns, setReadyColumns] = useState([]); const [readyColumns, setReadyColumns] = useState([]);
const eventBus = useContext(QueryToolEventsContext); const eventBus = useContext(QueryToolEventsContext);
const onSelectedColumnsChangeWrapped = (arg)=>{ const onSelectedColumnsChangeWrapped = (arg)=>{
@ -392,7 +399,7 @@ export default function QueryToolDataGrid({columns, rows, totalRowCount, dataCha
}, []); }, []);
const dataGridExtras = useMemo(()=>({ const dataGridExtras = useMemo(()=>({
onSelectedCellChange, handleCopy onSelectedCellChange, handleCopy, startRowNum
}), [onSelectedCellChange]); }), [onSelectedCellChange]);
useEffect(()=>{ useEffect(()=>{

View File

@ -276,10 +276,10 @@ export function MainToolBar({containerRef, onFilterClick, onManageMacros, onAddT
}, [queryToolConnCtx.connectionStatus]); }, [queryToolConnCtx.connectionStatus]);
const onCommitClick=()=>{ const onCommitClick=()=>{
eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, 'COMMIT;', null, '', true); eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, 'COMMIT;', {external: true});
}; };
const onRollbackClick=()=>{ const onRollbackClick=()=>{
eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, 'ROLLBACK;', null, '', true); eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, 'ROLLBACK;', {external: true});
}; };
const executeMacro = (m)=>{ const executeMacro = (m)=>{
eventBus.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_EXECUTION, null, m.sql); eventBus.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_EXECUTION, null, m.sql);

View File

@ -151,10 +151,10 @@ export default function Query({onTextSelect, handleEndOfLineChange}) {
query = query || editor.current?.getValue() || ''; query = query || editor.current?.getValue() || '';
} }
if(query) { if(query) {
eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, query, explainObject, macroSQL, external, null, executeCursor); eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, query, {explainObject, macroSQL, external, executeCursor});
} }
} else { } else {
eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, null, null, ''); eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, null, {});
} }
}; };

View File

@ -232,15 +232,20 @@ export class ResultSetUtils {
}; };
async startExecution(query, explainObject, macroSQL, onIncorrectSQL, flags={ async startExecution(query, explainObject, macroSQL, onIncorrectSQL, flags={
isQueryTool: true, external: false, reconnect: false, executeCursor: false isQueryTool: true, external: false, reconnect: false, executeCursor: false, refreshData: false,
}) { }) {
let startTime = new Date(); let startTime = new Date();
this.eventBus.fireEvent(QUERY_TOOL_EVENTS.SET_MESSAGE, ''); this.eventBus.fireEvent(QUERY_TOOL_EVENTS.SET_MESSAGE, '');
this.eventBus.fireEvent(QUERY_TOOL_EVENTS.TASK_START, gettext('Waiting for the query to complete...'), startTime); this.eventBus.fireEvent(QUERY_TOOL_EVENTS.TASK_START,
flags.refreshData ? gettext('Refetching latest results...') : gettext('Waiting for the query to complete...'),
startTime
);
this.setStartTime(startTime); this.setStartTime(startTime);
this.query = query; this.query = query;
this.historyQuerySource = flags.isQueryTool ? QuerySources.EXECUTE : QuerySources.VIEW_DATA; this.historyQuerySource = flags.isQueryTool ? QuerySources.EXECUTE : QuerySources.VIEW_DATA;
if(explainObject) { if(flags.refreshData) {
this.historyQuerySource = null;
} else if(explainObject) {
if(explainObject.analyze) { if(explainObject.analyze) {
this.historyQuerySource = QuerySources.EXPLAIN_ANALYZE; this.historyQuerySource = QuerySources.EXPLAIN_ANALYZE;
} else { } else {
@ -301,7 +306,7 @@ export class ResultSetUtils {
e, e,
{ {
connectionLostCallback: ()=>{ connectionLostCallback: ()=>{
this.eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, query, explainObject, '', flags.external, true, flags.executeCursor); this.eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, query, {explainObject, external: flags.external, reconnect: true, executeCursor: flags.executeCursor});
}, },
checkTransaction: true, checkTransaction: true,
} }
@ -357,7 +362,7 @@ export class ResultSetUtils {
}); });
this.eventBus.fireEvent(QUERY_TOOL_EVENTS.HANDLE_API_ERROR, error, { this.eventBus.fireEvent(QUERY_TOOL_EVENTS.HANDLE_API_ERROR, error, {
connectionLostCallback: ()=>{ connectionLostCallback: ()=>{
this.eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, this.query, explainObject, '', flags.external, true, flags.executeCursor); this.eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, this.query, {explainObject, external: flags.external, reconnect: true, executeCursor: flags.executeCursor});
}, },
checkTransaction: true, checkTransaction: true,
}); });
@ -396,7 +401,7 @@ export class ResultSetUtils {
if(this.qtPref?.query_success_notification) { if(this.qtPref?.query_success_notification) {
pgAdmin.Browser.notifier.success(msg); pgAdmin.Browser.notifier.success(msg);
} }
if(!ResultSetUtils.isQueryStillRunning(httpMessage)) { if(!ResultSetUtils.isQueryStillRunning(httpMessage) && this.historyQuerySource) {
this.eventBus.fireEvent(QUERY_TOOL_EVENTS.PUSH_HISTORY, { this.eventBus.fireEvent(QUERY_TOOL_EVENTS.PUSH_HISTORY, {
status: true, status: true,
start_time: this.startTime, start_time: this.startTime,
@ -414,16 +419,12 @@ export class ResultSetUtils {
} }
} }
getMoreRows(all=false) { getWindowRows(fromRownum, toRownum) {
let url = url_for('sqleditor.fetch', { let url = url_for('sqleditor.fetch_window', {
'trans_id': this.transId, 'trans_id': this.transId,
'from_rownum': fromRownum,
'to_rownum': toRownum,
}); });
if(all) {
url = url_for('sqleditor.fetch_all', {
'trans_id': this.transId,
'fetch_all': 1,
});
}
return this.api.get(url); return this.api.get(url);
} }
@ -791,6 +792,11 @@ function dataChangeReducer(state, action) {
...dataChange.deleted, ...dataChange.deleted,
...action.add, ...action.add,
}; };
if(action.all) {
dataChange.delete_all = true;
} else {
dataChange.delete_all = false;
}
break; break;
case 'reset': case 'reset':
dataChange = { dataChange = {
@ -798,6 +804,7 @@ function dataChangeReducer(state, action) {
added: {}, added: {},
added_index: {}, added_index: {},
deleted: {}, deleted: {},
delete_all: false,
}; };
break; break;
default: default:
@ -818,12 +825,14 @@ export function ResultSet() {
const [queryData, setQueryData] = useState(null); const [queryData, setQueryData] = useState(null);
const [rows, setRows] = useState([]); const [rows, setRows] = useState([]);
const [columns, setColumns] = useState([]); const [columns, setColumns] = useState([]);
const [isLoadingMore, setIsLoadingMore] = useState(false);
const api = getApiInstance(); const api = getApiInstance();
const rsu = React.useRef(new ResultSetUtils(api, queryToolCtx, queryToolCtx.params.trans_id, queryToolCtx.params.is_query_tool)); const rsu = React.useRef(new ResultSetUtils(api, queryToolCtx, queryToolCtx.params.trans_id, queryToolCtx.params.is_query_tool));
const [dataChangeStore, dispatchDataChange] = React.useReducer(dataChangeReducer, {}); const [dataChangeStore, dispatchDataChange] = React.useReducer(dataChangeReducer, {});
const [selectedRows, setSelectedRows] = useState(new Set()); const [selectedRows, setSelectedRows] = useState(new Set());
const [selectedColumns, setSelectedColumns] = useState(new Set()); const [selectedColumns, setSelectedColumns] = useState(new Set());
// NONE - no select, PAGE - show select all, ALL - select all.
const [allRowsSelect, setAllRowsSelect] = useState('NONE');
const selectedCell = useRef([]); const selectedCell = useRef([]);
const selectedRange = useRef(null); const selectedRange = useRef(null);
const setSelectedCell = (val)=>{ const setSelectedCell = (val)=>{
@ -855,7 +864,9 @@ export function ResultSet() {
eventBus.fireEvent(QUERY_TOOL_EVENTS.SELECTED_ROWS_COLS_CELL_CHANGED, selectedRows.size, selectedColumns.size, selectedRange.current, selectedCell.current?.length); eventBus.fireEvent(QUERY_TOOL_EVENTS.SELECTED_ROWS_COLS_CELL_CHANGED, selectedRows.size, selectedColumns.size, selectedRange.current, selectedCell.current?.length);
}; };
const executionStartCallback = async (query, explainObject, macroSQL, external=false, reconnect=false, executeCursor=false)=>{ const executionStartCallback = async (query, {
explainObject, macroSQL, external=false, reconnect=false, executeCursor=false, refreshData=false
})=>{
const yesCallback = async ()=>{ const yesCallback = async ()=>{
/* Reset */ /* Reset */
eventBus.fireEvent(QUERY_TOOL_EVENTS.HIGHLIGHT_ERROR, null); eventBus.fireEvent(QUERY_TOOL_EVENTS.HIGHLIGHT_ERROR, null);
@ -871,7 +882,7 @@ export function ResultSet() {
setColumns([]); setColumns([]);
setRows([]); setRows([]);
}, },
{isQueryTool: queryToolCtx.params.is_query_tool, external: external, reconnect: reconnect, executeCursor: executeCursor} {isQueryTool: queryToolCtx.params.is_query_tool, external: external, reconnect: reconnect, executeCursor: executeCursor, refreshData: refreshData}
); );
}; };
@ -916,7 +927,7 @@ export function ResultSet() {
}); });
}; };
if(isDataChanged()) { if(isDataChanged() && !refreshData) {
queryToolCtx.modal.confirm( queryToolCtx.modal.confirm(
gettext('Unsaved changes'), gettext('Unsaved changes'),
gettext('The data has been modified, but not saved. Are you sure you wish to discard the changes?'), gettext('The data has been modified, but not saved. Are you sure you wish to discard the changes?'),
@ -1010,19 +1021,41 @@ export function ResultSet() {
eventBus.registerListener(QUERY_TOOL_EVENTS.TRIGGER_INCLUDE_EXCLUDE_FILTER, triggerFilter); eventBus.registerListener(QUERY_TOOL_EVENTS.TRIGGER_INCLUDE_EXCLUDE_FILTER, triggerFilter);
eventBus.registerListener(QUERY_TOOL_EVENTS.GOTO_LAST_SCROLL, triggerResetScroll); eventBus.registerListener(QUERY_TOOL_EVENTS.GOTO_LAST_SCROLL, triggerResetScroll);
eventBus.registerListener(QUERY_TOOL_EVENTS.ALL_PAGE_ROWS_SELECTED, (selectAll)=>{
if(selectAll) {
setAllRowsSelect('PAGE');
} else {
setAllRowsSelect('NONE');
}
});
eventBus.registerListener(QUERY_TOOL_EVENTS.ALL_ROWS_SELECTED, ()=>{
setAllRowsSelect('ALL');
});
eventBus.registerListener(QUERY_TOOL_EVENTS.CLEAR_ROWS_SELECTED, ()=>{
setSelectedRows(new Set());
setAllRowsSelect('NONE');
});
}, []); }, []);
useEffect(()=>{ useEffect(()=>{
eventBus.registerListener(QUERY_TOOL_EVENTS.EXECUTION_START, executionStartCallback); const deregExec = eventBus.registerListener(QUERY_TOOL_EVENTS.EXECUTION_START, executionStartCallback);
return ()=>{ return ()=>{
eventBus.deregisterListener(QUERY_TOOL_EVENTS.EXECUTION_START, executionStartCallback); deregExec();
}; };
}, [dataChangeStore]); }, [dataChangeStore, dataOutputQuery]);
useEffect(()=>{ useEffect(()=>{
fireRowsColsCellChanged(); fireRowsColsCellChanged();
setAllRowsSelect('NONE');
}, [selectedRows.size, selectedColumns.size]); }, [selectedRows.size, selectedColumns.size]);
useEffect(()=>{
eventBus.fireEvent(QUERY_TOOL_EVENTS.ALL_ROWS_SELECTED_STATUS, allRowsSelect);
}, [allRowsSelect]);
useEffect(()=>{ useEffect(()=>{
rsu.current.transId = queryToolCtx.params.trans_id; rsu.current.transId = queryToolCtx.params.trans_id;
}, [queryToolCtx.params.trans_id]); }, [queryToolCtx.params.trans_id]);
@ -1031,45 +1064,44 @@ export function ResultSet() {
eventBus.fireEvent(QUERY_TOOL_EVENTS.RESET_GRAPH_VISUALISER, columns); eventBus.fireEvent(QUERY_TOOL_EVENTS.RESET_GRAPH_VISUALISER, columns);
}, [columns]); }, [columns]);
const fetchMoreRows = async (all=false, callback=undefined)=>{ const fetchWindow = async (fromRownum, toRownum, callback)=>{
if(queryData.has_more_rows) { let res = [];
let res = []; setLoaderText(gettext('Fetching rows...'));
setIsLoadingMore(true); try {
try { res = await rsu.current.getWindowRows(fromRownum, toRownum);
res = await rsu.current.getMoreRows(all); const newRows = rsu.current.processRows(res.data.data.result, columns);
const newRows = rsu.current.processRows(res.data.data.result, columns); setRows([...newRows]);
setRows((prevRows)=>[...prevRows, ...newRows]); setQueryData((prev)=>({
setQueryData((prev)=>({ ...prev,
...prev, pagination: res.data.data.pagination,
has_more_rows: res.data.data.has_more_rows, rows_fetched_to: res.data.data.rows_fetched_to!=0 ? res.data.data.rows_fetched_to : prev.rows_fetched_to,
rows_fetched_to: res.data.data.rows_fetched_to!=0 ? res.data.data.rows_fetched_to : prev.rows_fetched_to, }));
})); } catch (e) {
} catch (e) { eventBus.fireEvent(QUERY_TOOL_EVENTS.HANDLE_API_ERROR,
eventBus.fireEvent(QUERY_TOOL_EVENTS.HANDLE_API_ERROR, e,
e, {
{ connectionLostCallback: ()=>{
connectionLostCallback: ()=>{ eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, rsu.current.query, {external: false, reconnect: true});
eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, rsu.current.query, null, '', false, true); },
}, checkTransaction: true,
checkTransaction: true, }
} );
); } finally {
} finally { setLoaderText('');
setIsLoadingMore(false);
}
} }
callback?.(); callback?.();
}; };
useEffect(()=>{
eventBus.registerListener(QUERY_TOOL_EVENTS.FETCH_MORE_ROWS, fetchMoreRows);
return ()=>{
eventBus.deregisterListener(QUERY_TOOL_EVENTS.FETCH_MORE_ROWS, fetchMoreRows);
};
}, [queryData?.has_more_rows, columns]);
useEffect(()=>{ useEffect(()=>{
eventBus.fireEvent(QUERY_TOOL_EVENTS.ROWS_FETCHED, queryData?.rows_fetched_to, queryData?.rows_affected); eventBus.registerListener(QUERY_TOOL_EVENTS.FETCH_WINDOW, fetchWindow);
}, [queryData?.rows_fetched_to, queryData?.rows_affected]); return ()=>{
eventBus.deregisterListener(QUERY_TOOL_EVENTS.FETCH_WINDOW, fetchWindow);
};
}, [columns]);
useEffect(()=>{
eventBus.fireEvent(QUERY_TOOL_EVENTS.TOTAL_ROWS_COUNT, queryData?.rows_affected);
}, [queryData?.rows_affected]);
const warnSaveDataClose = ()=>{ const warnSaveDataClose = ()=>{
// No changes. // No changes.
@ -1116,6 +1148,7 @@ export function ResultSet() {
let {data: respData} = await rsu.current.saveData({ let {data: respData} = await rsu.current.saveData({
updated: dataChangeStore.updated, updated: dataChangeStore.updated,
deleted: dataChangeStore.deleted, deleted: dataChangeStore.deleted,
delete_all: dataChangeStore.delete_all,
added_index: dataChangeStore.added_index, added_index: dataChangeStore.added_index,
added: added, added: added,
columns: columns, columns: columns,
@ -1152,37 +1185,6 @@ export function ResultSet() {
} }
eventBus.fireEvent(QUERY_TOOL_EVENTS.SAVE_DATA_DONE, true); eventBus.fireEvent(QUERY_TOOL_EVENTS.SAVE_DATA_DONE, true);
if(_.size(dataChangeStore.added)) {
// Update the rows in a grid after addition
respData.data.query_results.forEach((qr)=>{
if(!_.isNull(qr.row_added)) {
let rowClientPK = Object.keys(qr.row_added)[0];
setRows((prevRows)=>{
let rowIdx = prevRows.findIndex((r)=>rowKeyGetter(r)==rowClientPK);
return [
...prevRows.slice(0, rowIdx),
{
...prevRows[rowIdx],
...qr.row_added[rowClientPK],
},
...prevRows.slice(rowIdx+1),
];
});
}
});
}
let deletedKeys = Object.keys(dataChangeStore.deleted);
if(deletedKeys.length == rows.length) {
setRows([]);
}
else if(deletedKeys.length > 0) {
setRows((prevRows)=>{
return prevRows.filter((row)=>{
return deletedKeys.indexOf(row[rsu.current.clientPK]) == -1;
});
});
setColumns((prev)=>prev);
}
dispatchDataChange({type: 'reset'}); dispatchDataChange({type: 'reset'});
setSelectedRows(new Set()); setSelectedRows(new Set());
setSelectedColumns(new Set()); setSelectedColumns(new Set());
@ -1192,6 +1194,8 @@ export function ResultSet() {
if(respData.data.transaction_status > CONNECTION_STATUS.TRANSACTION_STATUS_IDLE) { if(respData.data.transaction_status > CONNECTION_STATUS.TRANSACTION_STATUS_IDLE) {
pgAdmin.Browser.notifier.info(gettext('Auto-commit is off. You still need to commit changes to the database.')); pgAdmin.Browser.notifier.info(gettext('Auto-commit is off. You still need to commit changes to the database.'));
} }
eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, rsu.current.query, {refreshData: true});
} catch (error) { } catch (error) {
eventBus.fireEvent(QUERY_TOOL_EVENTS.SAVE_DATA_DONE, false); eventBus.fireEvent(QUERY_TOOL_EVENTS.SAVE_DATA_DONE, false);
eventBus.fireEvent(QUERY_TOOL_EVENTS.HANDLE_API_ERROR, error, { eventBus.fireEvent(QUERY_TOOL_EVENTS.HANDLE_API_ERROR, error, {
@ -1292,6 +1296,7 @@ export function ResultSet() {
type: 'deleted', type: 'deleted',
add: add, add: add,
remove: remove, remove: remove,
all: remove.length > 0 ? false : allRowsSelect == 'ALL',
}); });
}; };
@ -1307,7 +1312,7 @@ export function ResultSet() {
return ()=>{ return ()=>{
eventBus.deregisterListener(QUERY_TOOL_EVENTS.TRIGGER_DELETE_ROWS, triggerDeleteRows); eventBus.deregisterListener(QUERY_TOOL_EVENTS.TRIGGER_DELETE_ROWS, triggerDeleteRows);
}; };
}, [selectedRows, queryData, dataChangeStore, rows]); }, [selectedRows, queryData, dataChangeStore, rows, allRowsSelect]);
useEffect(()=>{ useEffect(()=>{
const triggerAddRows = (_rows, fromClipboard, pasteSerials)=>{ const triggerAddRows = (_rows, fromClipboard, pasteSerials)=>{
@ -1317,7 +1322,7 @@ export function ResultSet() {
selectedRowsSorted.sort(); selectedRowsSorted.sort();
insPosn = _.findIndex(rows, (r)=>rowKeyGetter(r)==selectedRowsSorted[selectedRowsSorted.length-1])+1; insPosn = _.findIndex(rows, (r)=>rowKeyGetter(r)==selectedRowsSorted[selectedRowsSorted.length-1])+1;
} }
let byteaCellSelection = columns.filter(o=>o.type=='bytea'); let byteaCellSelection = columns.filter(o=>o.type=='bytea');
if (byteaCellSelection.length>0) { if (byteaCellSelection.length>0) {
_rows = _rows.map(x=>{ _rows = _rows.map(x=>{
byteaCellSelection.forEach(r=>{ byteaCellSelection.forEach(r=>{
@ -1373,20 +1378,6 @@ export function ResultSet() {
return ()=>eventBus.deregisterListener(QUERY_TOOL_EVENTS.TRIGGER_RENDER_GEOMETRIES, renderGeometries); return ()=>eventBus.deregisterListener(QUERY_TOOL_EVENTS.TRIGGER_RENDER_GEOMETRIES, renderGeometries);
}, [rows, columns, selectedRows.size, selectedColumns.size]); }, [rows, columns, selectedRows.size, selectedColumns.size]);
const handleScroll = (e) => {
// Set scroll current position of RestSet.
if (!_.isNull(e.currentTarget) && isResettingScroll.current) {
lastScrollRef.current = {
ref: { ...e },
top: e.currentTarget.scrollTop,
left: e.currentTarget.scrollLeft
};
}
if (isLoadingMore || !rsu.current.isAtBottom(e)) return;
eventBus.fireEvent(QUERY_TOOL_EVENTS.FETCH_MORE_ROWS);
};
const triggerResetScroll = () => { const triggerResetScroll = () => {
// Reset the scroll position to previously saved location. // Reset the scroll position to previously saved location.
if (lastScrollRef.current) { if (lastScrollRef.current) {
@ -1463,17 +1454,20 @@ export function ResultSet() {
return ( return (
<StyledBox ref={containerRef} tabIndex="0"> <StyledBox ref={containerRef} tabIndex="0">
<Loader message={loaderText} /> <Loader message={loaderText} />
<Loader data-label="loader-more-rows" message={isLoadingMore ? gettext('Loading more rows...') : null} style={{top: 'unset', right: 'unset', padding: '0.5rem 1rem'}}/>
{!queryData && {!queryData &&
<EmptyPanelMessage text={gettext('No data output. Execute a query to get output.')}/> <EmptyPanelMessage text={gettext('No data output. Execute a query to get output.')}/>
} }
{queryData && <> {queryData && <>
<ResultSetToolbar containerRef={containerRef} query={dataOutputQuery} canEdit={queryData.can_edit} totalRowCount={queryData?.rows_affected}/> <ResultSetToolbar containerRef={containerRef} query={dataOutputQuery}
canEdit={queryData.can_edit} totalRowCount={queryData?.rows_affected}
pagination={queryData?.pagination ?? {}} allRowsSelect={allRowsSelect}
/>
<Box flexGrow="1" minHeight="0"> <Box flexGrow="1" minHeight="0">
<QueryToolDataGrid <QueryToolDataGrid
columns={columns} columns={columns}
rows={rows} rows={rows}
totalRowCount={queryData?.rows_affected} totalRowCount={queryData?.rows_affected}
startRowNum={queryData?.pagination?.rows_from}
columnWidthBy={ columnWidthBy={
queryToolCtx.preferences?.sqleditor?.column_data_auto_resize == 'by_data' ? queryToolCtx.preferences?.sqleditor?.column_data_auto_resize == 'by_data' ?
queryToolCtx.preferences.sqleditor.column_data_max_width : queryToolCtx.preferences.sqleditor.column_data_max_width :
@ -1481,7 +1475,6 @@ export function ResultSet() {
} }
key={rowsResetKey} key={rowsResetKey}
rowKeyGetter={rowKeyGetter} rowKeyGetter={rowKeyGetter}
onScroll={handleScroll}
onRowsChange={onRowsChange} onRowsChange={onRowsChange}
dataChangeStore={dataChangeStore} dataChangeStore={dataChangeStore}
selectedRows={selectedRows} selectedRows={selectedRows}

View File

@ -8,8 +8,8 @@
////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////
import React, {useContext, useCallback, useEffect, useState} from 'react'; import React, {useContext, useCallback, useEffect, useState} from 'react';
import { styled } from '@mui/material/styles'; import { styled } from '@mui/material/styles';
import { Portal } from '@mui/material'; import { Box, Portal } from '@mui/material';
import { PgButtonGroup, PgIconButton } from '../../../../../../static/js/components/Buttons'; import { DefaultButton, PgButtonGroup, PgIconButton } from '../../../../../../static/js/components/Buttons';
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
import PlaylistAddRoundedIcon from '@mui/icons-material/PlaylistAddRounded'; import PlaylistAddRoundedIcon from '@mui/icons-material/PlaylistAddRounded';
import FileCopyRoundedIcon from '@mui/icons-material/FileCopyRounded'; import FileCopyRoundedIcon from '@mui/icons-material/FileCopyRounded';
@ -17,6 +17,14 @@ import DeleteRoundedIcon from '@mui/icons-material/DeleteRounded';
import TimelineRoundedIcon from '@mui/icons-material/TimelineRounded'; import TimelineRoundedIcon from '@mui/icons-material/TimelineRounded';
import { PasteIcon, SQLQueryIcon, SaveDataIcon } from '../../../../../../static/js/components/ExternalIcon'; import { PasteIcon, SQLQueryIcon, SaveDataIcon } from '../../../../../../static/js/components/ExternalIcon';
import GetAppRoundedIcon from '@mui/icons-material/GetAppRounded'; import GetAppRoundedIcon from '@mui/icons-material/GetAppRounded';
import FastForwardRoundedIcon from '@mui/icons-material/FastForwardRounded';
import FastRewindRoundedIcon from '@mui/icons-material/FastRewindRounded';
import SkipNextRoundedIcon from '@mui/icons-material/SkipNextRounded';
import SkipPreviousRoundedIcon from '@mui/icons-material/SkipPreviousRounded';
import EditRoundedIcon from '@mui/icons-material/EditRounded';
import EditOffRoundedIcon from '@mui/icons-material/EditOffRounded';
import CheckRoundedIcon from '@mui/icons-material/CheckRounded';
import {QUERY_TOOL_EVENTS} from '../QueryToolConstants'; import {QUERY_TOOL_EVENTS} from '../QueryToolConstants';
import { QueryToolContext, QueryToolEventsContext } from '../QueryToolComponent'; import { QueryToolContext, QueryToolEventsContext } from '../QueryToolComponent';
import { PgMenu, PgMenuItem } from '../../../../../../static/js/components/Menu'; import { PgMenu, PgMenuItem } from '../../../../../../static/js/components/Menu';
@ -26,14 +34,28 @@ import CopyData from '../QueryToolDataGrid/CopyData';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import CodeMirror from '../../../../../../static/js/components/ReactCodeMirror'; import CodeMirror from '../../../../../../static/js/components/ReactCodeMirror';
import { setEditorPosition } from '../QueryToolDataGrid/Editors'; import { setEditorPosition } from '../QueryToolDataGrid/Editors';
import { InputText } from '../../../../../../static/js/components/FormComponents';
import { minMaxValidator } from '../../../../../../static/js/validators';
const StyledDiv = styled('div')(({theme})=>({ const StyledDiv = styled('div')(({theme})=>({
padding: '2px', padding: '2px',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
gap: '4px', flexWrap: 'wrap',
rowGap: '4px',
backgroundColor: theme.otherVars.editorToolbarBg, backgroundColor: theme.otherVars.editorToolbarBg,
justifyContent: 'space-between',
...theme.mixins.panelBorder.bottom, ...theme.mixins.panelBorder.bottom,
'& .PaginationInputs': {
display: 'flex',
alignItems: 'center',
gap: '4px',
'& .PaginationInputs-divider': {
...theme.mixins.panelBorder.right,
}
}
})); }));
const StyledEditor = styled('div')(({theme})=>({ const StyledEditor = styled('div')(({theme})=>({
@ -76,7 +98,134 @@ ShowDataOutputQueryPopup.propTypes = {
query: PropTypes.string, query: PropTypes.string,
}; };
export function ResultSetToolbar({query,canEdit, totalRowCount}) {
function PaginationInputs({pagination, totalRowCount, clearSelection}) {
const eventBus = useContext(QueryToolEventsContext);
const [editPageRange, setEditPageRange] = useState(false);
const [errorInputs, setErrorInputs] = useState({
'from': false,
'to': false,
'pageNo': false
});
const [inputs, setInputs] = useState({
from: pagination.rows_from ?? 0,
to: pagination.rows_to ?? 0,
pageNo: pagination.page_no ?? 0,
});
const goToPage = (pageNo)=>{
const from = (pageNo-1) * pagination.page_size + 1;
const to = from + pagination.page_size - 1;
eventBus.fireEvent(QUERY_TOOL_EVENTS.FETCH_WINDOW, from, to);
clearSelection();
};
const onInputChange = (key, value)=>{
setInputs((prev)=>({...prev, [key]: value}));
};
const onInputKeydown = (e)=>{
if(e.code === 'Enter' && !errorInputs.from && !errorInputs.to) {
e.preventDefault();
eventBus.fireEvent(QUERY_TOOL_EVENTS.FETCH_WINDOW, inputs.from, inputs.to);
}
};
const onInputKeydownPageNo = (e)=>{
if(e.code === 'Enter' && !errorInputs.pageNo) {
e.preventDefault();
goToPage(inputs.pageNo);
}
};
useEffect(()=>{
// validate
setErrorInputs((prev)=>{
let errors = {...prev};
if(minMaxValidator('', inputs.pageNo, 1, pagination.page_count)) {
errors.pageNo = true;
} else {
errors.pageNo = false;
}
if(minMaxValidator('', inputs.from, 1, inputs.to)) {
errors.from = true;
} else {
errors.from = false;
}
if(minMaxValidator('', inputs.to, 1, totalRowCount)) {
errors.to = true;
} else {
errors.to = false;
}
return errors;
});
}, [inputs, pagination]);
return (
<Box className='PaginationInputs'>
{editPageRange ?
<Box display="flex" gap="2px" alignItems="center">
<div>{gettext('Showing rows:')}</div>
<InputText size="small"
controlProps={{maxLength: 7}}
style={{
maxWidth: '10ch'
}}
value={inputs.from}
onChange={(value)=>onInputChange('from', value)}
onKeyDown={onInputKeydown}
error={errorInputs['from']}
/>
<div>{gettext('to')}</div>
<InputText size="small"
controlProps={{maxLength: 7}}
style={{
maxWidth: '10ch'
}}
value={inputs.to}
onChange={(value)=>onInputChange('to', value)}
onKeyDown={onInputKeydown}
error={errorInputs['to']}
/>
</Box> : <span>{gettext('Showing rows: %s to %s', inputs.from, inputs.to)}</span>}
<PgButtonGroup>
{editPageRange && <PgIconButton size="xs"
title={editPageRange ? gettext('Apply (or press Enter on input)') : gettext('Edit range')}
onClick={()=>eventBus.fireEvent(QUERY_TOOL_EVENTS.FETCH_WINDOW, inputs.from, inputs.to)}
icon={<CheckRoundedIcon />}
/>}
<PgIconButton size="xs"
title={editPageRange ? gettext('Cancel edit') : gettext('Edit range')}
onClick={()=>setEditPageRange((prev)=>!prev)}
icon={editPageRange ? <EditOffRoundedIcon /> : <EditRoundedIcon />}
/>
</PgButtonGroup>
<div className='PaginationInputs-divider'>&nbsp;</div>
<span>{gettext('Page No:')}</span>
<InputText size="small"
controlProps={{maxLength: 7}}
style={{
maxWidth: '10ch'
}}
value={inputs.pageNo}
onChange={(value)=>onInputChange('pageNo', value)}
onKeyDown={onInputKeydownPageNo}
error={errorInputs['pageNo']}
/>
<span> {gettext('of')} {pagination.page_count}</span>
<div className='PaginationInputs-divider'>&nbsp;</div>
<PgButtonGroup size="small">
<PgIconButton title={gettext('First Page')} disabled={pagination.page_no == 1} onClick={()=>goToPage(1)} icon={<SkipPreviousRoundedIcon />}/>
<PgIconButton title={gettext('Previous Page')} disabled={pagination.page_no == 1} onClick={()=>goToPage(pagination.page_no-1)} icon={<FastRewindRoundedIcon />}/>
<PgIconButton title={gettext('Next Page')} disabled={pagination.page_no == pagination.page_count} onClick={()=>goToPage(pagination.page_no+1)} icon={<FastForwardRoundedIcon />}/>
<PgIconButton title={gettext('Last Page')} disabled={pagination.page_no == pagination.page_count} onClick={()=>goToPage(pagination.page_count)} icon={<SkipNextRoundedIcon />} />
</PgButtonGroup>
</Box>
);
}
export function ResultSetToolbar({query, canEdit, totalRowCount, pagination, allRowsSelect}) {
const eventBus = useContext(QueryToolEventsContext); const eventBus = useContext(QueryToolEventsContext);
const queryToolCtx = useContext(QueryToolContext); const queryToolCtx = useContext(QueryToolContext);
const [dataOutputQueryBtn,setDataOutputQueryBtn] = useState(false); const [dataOutputQueryBtn,setDataOutputQueryBtn] = useState(false);
@ -208,45 +357,74 @@ export function ResultSetToolbar({query,canEdit, totalRowCount}) {
}, },
], queryToolCtx.mainContainerRef); ], queryToolCtx.mainContainerRef);
const clearSelection = ()=>{
eventBus.fireEvent(QUERY_TOOL_EVENTS.CLEAR_ROWS_SELECTED);
};
return ( return (
<> <>
<StyledDiv> <StyledDiv>
<PgButtonGroup size="small"> <Box display="flex" alignItems="center" gap="4px">
<PgIconButton title={gettext('Add row')} icon={<PlaylistAddRoundedIcon style={{height: 'unset'}}/>}
shortcut={queryToolPref.btn_add_row} disabled={!canEdit} onClick={addRow} />
<PgIconButton title={gettext('Copy')} icon={<FileCopyRoundedIcon />}
shortcut={FIXED_PREF.copy} disabled={buttonsDisabled['copy-rows']} onClick={copyData} />
<PgIconButton title={gettext('Copy options')} icon={<KeyboardArrowDownIcon />} splitButton
name="menu-copyheader" ref={copyMenuRef} onClick={openMenu} />
<PgIconButton title={gettext('Paste')} icon={<PasteIcon />}
shortcut={queryToolPref.btn_paste_row} disabled={!canEdit} onClick={pasteRows} />
<PgIconButton title={gettext('Paste options')} icon={<KeyboardArrowDownIcon />} splitButton
name="menu-pasteoptions" ref={pasetMenuRef} onClick={openMenu} />
<PgIconButton title={gettext('Delete')} icon={<DeleteRoundedIcon />}
shortcut={queryToolPref.btn_delete_row} disabled={buttonsDisabled['delete-rows'] || !canEdit} onClick={deleteRows} />
</PgButtonGroup>
<PgButtonGroup size="small">
<PgIconButton title={gettext('Save Data Changes')} icon={<SaveDataIcon />}
shortcut={queryToolPref.save_data} disabled={buttonsDisabled['save-data'] || !canEdit} onClick={saveData}/>
</PgButtonGroup>
<PgButtonGroup size="small">
<PgIconButton title={gettext('Save results to file')} icon={<GetAppRoundedIcon />}
onClick={downloadResult} shortcut={queryToolPref.download_results}
disabled={buttonsDisabled['save-result']} />
</PgButtonGroup>
<PgButtonGroup size="small">
<PgIconButton title={gettext('Graph Visualiser')} icon={<TimelineRoundedIcon />}
onClick={showGraphVisualiser} disabled={buttonsDisabled['save-result']} />
</PgButtonGroup>
{query &&
<>
<PgButtonGroup size="small"> <PgButtonGroup size="small">
<PgIconButton title={gettext('SQL query of data')} icon={<SQLQueryIcon />} <PgIconButton title={gettext('Add row')} icon={<PlaylistAddRoundedIcon style={{height: 'unset'}}/>}
onClick={()=>{setDataOutputQueryBtn(prev=>!prev);}} onBlur={()=>{setDataOutputQueryBtn(false);}} disabled={!query} id='sql-query'/> shortcut={queryToolPref.btn_add_row} disabled={!canEdit} onClick={addRow} />
<PgIconButton title={gettext('Copy')} icon={<FileCopyRoundedIcon />}
shortcut={FIXED_PREF.copy} disabled={buttonsDisabled['copy-rows']||allRowsSelect=='ALL'} onClick={copyData} />
<PgIconButton title={gettext('Copy options')} icon={<KeyboardArrowDownIcon />} splitButton
name="menu-copyheader" ref={copyMenuRef} onClick={openMenu} />
<PgIconButton title={gettext('Paste')} icon={<PasteIcon />}
shortcut={queryToolPref.btn_paste_row} disabled={!canEdit} onClick={pasteRows} />
<PgIconButton title={gettext('Paste options')} icon={<KeyboardArrowDownIcon />} splitButton
name="menu-pasteoptions" ref={pasetMenuRef} onClick={openMenu} />
<PgIconButton title={gettext('Delete')} icon={<DeleteRoundedIcon />}
shortcut={queryToolPref.btn_delete_row} disabled={buttonsDisabled['delete-rows'] || !canEdit} onClick={deleteRows} />
</PgButtonGroup> </PgButtonGroup>
{ dataOutputQueryBtn && <ShowDataOutputQueryPopup query={query} />} <PgButtonGroup size="small">
</> <PgIconButton title={gettext('Save Data Changes')} icon={<SaveDataIcon />}
} shortcut={queryToolPref.save_data} disabled={buttonsDisabled['save-data'] || !canEdit} onClick={saveData}/>
</PgButtonGroup>
<PgButtonGroup size="small">
<PgIconButton title={gettext('Save results to file')} icon={<GetAppRoundedIcon />}
onClick={downloadResult} shortcut={queryToolPref.download_results}
disabled={buttonsDisabled['save-result']} />
</PgButtonGroup>
<PgButtonGroup size="small">
<PgIconButton title={gettext('Graph Visualiser')} icon={<TimelineRoundedIcon />}
onClick={showGraphVisualiser} disabled={buttonsDisabled['save-result']} />
</PgButtonGroup>
{query &&
<>
<PgButtonGroup size="small">
<PgIconButton title={gettext('SQL query of data')} icon={<SQLQueryIcon />}
onClick={()=>{setDataOutputQueryBtn(prev=>!prev);}} onBlur={()=>{setDataOutputQueryBtn(false);}} disabled={!query} id='sql-query'/>
</PgButtonGroup>
{ dataOutputQueryBtn && <ShowDataOutputQueryPopup query={query} />}
</>
}
{
allRowsSelect == 'PAGE' && (
<div>
<span>{gettext('All rows on this page are selected.')}</span>
<PgButtonGroup size="small">
<DefaultButton onClick={()=>eventBus.fireEvent(QUERY_TOOL_EVENTS.ALL_ROWS_SELECTED)}>Select All {totalRowCount} Rows</DefaultButton>
</PgButtonGroup>
</div>
)
}
{
allRowsSelect == 'ALL' && (
<div>
<span>{gettext('All %s rows are selected.', totalRowCount)}</span>
<PgButtonGroup size="small">
<DefaultButton onClick={clearSelection}>{gettext('Clear Selection')}</DefaultButton>
</PgButtonGroup>
</div>
)
}
</Box>
<Box>
<PaginationInputs key={JSON.stringify(pagination)} pagination={pagination} totalRowCount={totalRowCount} clearSelection={clearSelection} />
</Box>
</StyledDiv> </StyledDiv>
<PgMenu <PgMenu
anchorRef={copyMenuRef} anchorRef={copyMenuRef}

View File

@ -41,7 +41,7 @@ export function StatusBar({eol, handleEndOfLineChange}) {
const eventBus = useContext(QueryToolEventsContext); const eventBus = useContext(QueryToolEventsContext);
const [position, setPosition] = useState([1, 1]); const [position, setPosition] = useState([1, 1]);
const [lastTaskText, setLastTaskText] = useState(null); const [lastTaskText, setLastTaskText] = useState(null);
const [rowsCount, setRowsCount] = useState([0, 0]); const [rowsCount, setRowsCount] = useState(0);
const [selectedRowsCount, setSelectedRowsCount] = useState(0); const [selectedRowsCount, setSelectedRowsCount] = useState(0);
const [dataRowChangeCounts, setDataRowChangeCounts] = useState({ const [dataRowChangeCounts, setDataRowChangeCounts] = useState({
isDirty: false, isDirty: false,
@ -52,6 +52,8 @@ export function StatusBar({eol, handleEndOfLineChange}) {
const {seconds, minutes, hours, msec, start:startTimer, pause:pauseTimer, reset:resetTimer} = useStopwatch({}); const {seconds, minutes, hours, msec, start:startTimer, pause:pauseTimer, reset:resetTimer} = useStopwatch({});
const eolMenuRef = React.useRef(null); const eolMenuRef = React.useRef(null);
const {openMenuName, toggleMenu, onMenuClose} = usePgMenuGroup(); const {openMenuName, toggleMenu, onMenuClose} = usePgMenuGroup();
// NONE - no select, PAGE - show select all, ALL - select all.
const [allRowsSelect, setAllRowsSelect] = useState('NONE');
useEffect(()=>{ useEffect(()=>{
eventBus.registerListener(QUERY_TOOL_EVENTS.CURSOR_ACTIVITY, (newPos)=>{ eventBus.registerListener(QUERY_TOOL_EVENTS.CURSOR_ACTIVITY, (newPos)=>{
@ -70,20 +72,30 @@ export function StatusBar({eol, handleEndOfLineChange}) {
pauseTimer(endTime); pauseTimer(endTime);
setLastTaskText(taskText); setLastTaskText(taskText);
}); });
eventBus.registerListener(QUERY_TOOL_EVENTS.ROWS_FETCHED, (fetched, total)=>{ eventBus.registerListener(QUERY_TOOL_EVENTS.TOTAL_ROWS_COUNT, (total)=>{
setRowsCount([fetched||0, total||0]); setRowsCount(total);
});
eventBus.registerListener(QUERY_TOOL_EVENTS.ALL_ROWS_SELECTED_STATUS, (v)=>{
setAllRowsSelect(v);
}); });
eventBus.registerListener(QUERY_TOOL_EVENTS.SELECTED_ROWS_COLS_CELL_CHANGED, (rows)=>{ eventBus.registerListener(QUERY_TOOL_EVENTS.SELECTED_ROWS_COLS_CELL_CHANGED, (rows)=>{
setSelectedRowsCount(rows); setSelectedRowsCount(rows);
}); });
eventBus.registerListener(QUERY_TOOL_EVENTS.DATAGRID_CHANGED, (_isDirty, dataChangeStore)=>{ }, []);
useEffect(()=>{
const unregDataChange = eventBus.registerListener(QUERY_TOOL_EVENTS.DATAGRID_CHANGED, (_isDirty, dataChangeStore)=>{
setDataRowChangeCounts({ setDataRowChangeCounts({
added: Object.keys(dataChangeStore.added||{}).length, added: Object.keys(dataChangeStore.added||{}).length,
updated: Object.keys(dataChangeStore.updated||{}).length, updated: Object.keys(dataChangeStore.updated||{}).length,
deleted: Object.keys(dataChangeStore.deleted||{}).length, deleted: dataChangeStore.delete_all ? rowsCount : Object.keys(dataChangeStore.deleted||{}).length,
}); });
}); });
}, []);
return ()=>{
unregDataChange();
};
}, [rowsCount]);
let stagedText = ''; let stagedText = '';
if(dataRowChangeCounts.added > 0) { if(dataRowChangeCounts.added > 0) {
@ -98,7 +110,7 @@ export function StatusBar({eol, handleEndOfLineChange}) {
return ( return (
<StyledBox> <StyledBox>
<Box className='StatusBar-padding StatusBar-divider'>{gettext('Total rows: %s of %s', rowsCount[0], rowsCount[1])}</Box> {rowsCount && <Box className='StatusBar-padding StatusBar-divider'>{gettext('Total rows: %s', rowsCount)}</Box>}
{lastTaskText && {lastTaskText &&
<Box className='StatusBar-padding StatusBar-divider'>{lastTaskText} {hours.toString().padStart(2, '0')}:{minutes.toString().padStart(2, '0')}:{seconds.toString().padStart(2, '0')}.{msec.toString().padStart(3, '0')}</Box> <Box className='StatusBar-padding StatusBar-divider'>{lastTaskText} {hours.toString().padStart(2, '0')}:{minutes.toString().padStart(2, '0')}:{seconds.toString().padStart(2, '0')}.{msec.toString().padStart(3, '0')}</Box>
} }
@ -106,13 +118,13 @@ export function StatusBar({eol, handleEndOfLineChange}) {
<Box className='StatusBar-padding StatusBar-divider'>{lastTaskText} {hours.toString().padStart(2, '0')}:{minutes.toString().padStart(2, '0')}:{seconds.toString().padStart(2, '0')}.{msec.toString().padStart(3, '0')}</Box> <Box className='StatusBar-padding StatusBar-divider'>{lastTaskText} {hours.toString().padStart(2, '0')}:{minutes.toString().padStart(2, '0')}:{seconds.toString().padStart(2, '0')}.{msec.toString().padStart(3, '0')}</Box>
} }
{Boolean(selectedRowsCount) && {Boolean(selectedRowsCount) &&
<Box className='StatusBar-padding StatusBar-divider'>{gettext('Rows selected: %s',selectedRowsCount)}</Box>} <Box className='StatusBar-padding StatusBar-divider'>{gettext('Rows selected: %s', allRowsSelect == 'ALL' ? rowsCount : selectedRowsCount)}</Box>}
{stagedText && {stagedText &&
<Box className='StatusBar-padding StatusBar-divider'> <Box className='StatusBar-padding StatusBar-divider'>
<span>{gettext('Changes staged: %s', stagedText)}</span> <span>{gettext('Changes staged: %s', stagedText)}</span>
</Box> </Box>
} }
<Box className='StatusBar-padding StatusBar-mlAuto' style={{display:'flex'}}> <Box className='StatusBar-padding StatusBar-mlAuto' style={{display:'flex'}}>
<Box className="StatusBar-padding StatusBar-divider"> <Box className="StatusBar-padding StatusBar-divider">
<Tooltip title="Select EOL Sequence" disableInteractive enterDelay={2500}> <Tooltip title="Select EOL Sequence" disableInteractive enterDelay={2500}>

View File

@ -6,8 +6,8 @@ DELETE FROM {{ conn|qtIdent(nsp_name, object_name) }}
{% elif no_of_keys > 1 %} {% elif no_of_keys > 1 %}
WHERE ({% for each_label in primary_key_labels %}{{ conn|qtIdent(each_label) }}{% if not loop.last %}, {% endif %}{% endfor %}) IN WHERE ({% for each_label in primary_key_labels %}{{ conn|qtIdent(each_label) }}{% if not loop.last %}, {% endif %}{% endfor %}) IN
{% endif %} {% endif %}
{### Rows to delete ###} {% if no_of_keys >= 1 %}{### Rows to delete ###}
({% for obj in data %}{% if no_of_keys == 1 %}{{ obj[primary_key_labels[0]]|qtLiteral(conn) }}{% elif no_of_keys > 1 %} ({% for obj in data %}{% if no_of_keys == 1 %}{{ obj[primary_key_labels[0]]|qtLiteral(conn) }}{% elif no_of_keys > 1 %}
{### Here we need to make tuple for each row ###} {### Here we need to make tuple for each row ###}
({% for each_label in primary_key_labels %}{{ obj[each_label]|qtLiteral(conn) }}{% if not loop.last %}, {% endif %}{% endfor %}){% endif %}{% if not loop.last %}, {% endif %} ({% for each_label in primary_key_labels %}{{ obj[each_label]|qtLiteral(conn) }}{% if not loop.last %}, {% endif %}{% endfor %}){% endif %}{% if not loop.last %}, {% endif %}
{% endfor %}); {% endfor %}){% endif %};

View File

@ -14,7 +14,7 @@ from pgadmin.utils.constants import PREF_LABEL_DISPLAY,\
PREF_LABEL_EDITOR, PREF_LABEL_CSV_TXT, PREF_LABEL_RESULTS_GRID,\ PREF_LABEL_EDITOR, PREF_LABEL_CSV_TXT, PREF_LABEL_RESULTS_GRID,\
PREF_LABEL_SQL_FORMATTING, PREF_LABEL_GRAPH_VISUALISER PREF_LABEL_SQL_FORMATTING, PREF_LABEL_GRAPH_VISUALISER
from pgadmin.utils import SHORTCUT_FIELDS as shortcut_fields from pgadmin.utils import SHORTCUT_FIELDS as shortcut_fields
from config import ON_DEMAND_RECORD_COUNT from config import DATA_RESULT_ROWS_PER_PAGE
UPPER_CASE_STR = gettext('Upper case') UPPER_CASE_STR = gettext('Upper case')
LOWER_CASE_STR = gettext('Lower case') LOWER_CASE_STR = gettext('Lower case')
@ -346,15 +346,15 @@ def register_query_tool_preferences(self):
), ),
) )
self.on_demand_record_count = self.preference.register( self.data_result_rows_per_page = self.preference.register(
'Results_grid', 'on_demand_record_count', 'Results_grid', 'data_result_rows_per_page',
gettext("On demand record count"), 'integer', ON_DEMAND_RECORD_COUNT, gettext("Data result rows per page"), 'integer',
min_val=1, DATA_RESULT_ROWS_PER_PAGE, min_val=10,
category_label=PREF_LABEL_RESULTS_GRID, category_label=PREF_LABEL_RESULTS_GRID,
help_str=gettext('Specify the number of records to fetch in one batch ' help_str=gettext('Specify the number of records to fetch in one batch.'
'in query tool when query result set is large. ' ' Changing this value will override'
'Changing this value will override ' ' DATA_RESULT_ROWS_PER_PAGE setting from config '
'ON_DEMAND_RECORD_COUNT setting from config file.') ' file.')
) )
self.sql_font_size = self.preference.register( self.sql_font_size = self.preference.register(

View File

@ -188,35 +188,37 @@ def save_changed_data(changed_data, columns_info, conn, command_obj,
# For deleted rows # For deleted rows
elif of_type == 'deleted': elif of_type == 'deleted':
delete_all = changed_data.get('delete_all', False)
list_of_sql[of_type] = [] list_of_sql[of_type] = []
is_first = True is_first = True
rows_to_delete = [] rows_to_delete = []
keys = None keys = []
no_of_keys = None no_of_keys = 0
for each_row in changed_data[of_type]: if not delete_all:
rows_to_delete.append(changed_data[of_type][each_row]) for each_row in changed_data[of_type]:
# Fetch the keys for SQL generation rows_to_delete.append(changed_data[of_type][each_row])
if is_first: # Fetch the keys for SQL generation
# We need to covert dict_keys to normal list in if is_first:
# Python3 # We need to covert dict_keys to normal list in
# In Python2, it's already a list & We will also # Python3
# fetch column names using index # In Python2, it's already a list & We will also
keys = list( # fetch column names using index
changed_data[of_type][each_row].keys() keys = list(
) changed_data[of_type][each_row].keys()
no_of_keys = len(keys) )
is_first = False no_of_keys = len(keys)
# Map index with column name for each row is_first = False
for row in rows_to_delete: # Map index with column name for each row
for k, v in row.items(): for row in rows_to_delete:
# Set primary key with label & delete index based for k, v in row.items():
# mapped key # Set primary key with label & delete index based
try: # mapped key
row[changed_data['columns'] try:
[int(k)]['name']] = v row[changed_data['columns']
except ValueError: [int(k)]['name']] = v
continue except ValueError:
del row[k] continue
del row[k]
sql = render_template( sql = render_template(
"/".join([command_obj.sql_path, 'delete.sql']), "/".join([command_obj.sql_path, 'delete.sql']),

View File

@ -1308,6 +1308,7 @@ WHERE db.datname = current_database()""")
return True, {'columns': columns, 'rows': rows} return True, {'columns': columns, 'rows': rows}
def async_fetchmany_2darray(self, records=2000, def async_fetchmany_2darray(self, records=2000,
from_rownum=0, to_rownum=0,
formatted_exception_msg=False): formatted_exception_msg=False):
""" """
User should poll and check if status is ASYNC_OK before calling this User should poll and check if status is ASYNC_OK before calling this
@ -1342,6 +1343,10 @@ WHERE db.datname = current_database()""")
try: try:
if records == -1: if records == -1:
result = cur.fetchall(_tupples=True) result = cur.fetchall(_tupples=True)
elif records is None:
result = cur.fetchwindow(from_rownum=from_rownum,
to_rownum=to_rownum,
_tupples=True)
else: else:
result = cur.fetchmany(records, _tupples=True) result = cur.fetchmany(records, _tupples=True)
except psycopg.ProgrammingError: except psycopg.ProgrammingError:
@ -1538,6 +1543,12 @@ Failed to reset the connection to the server due to following error:
return self.row_count return self.row_count
@property
def total_rows(self):
if self.__async_cursor is None:
return 0
return self.__async_cursor.rowcount
def get_column_info(self): def get_column_info(self):
""" """
This function will returns list of columns for last async sql command This function will returns list of columns for last async sql command

View File

@ -364,6 +364,20 @@ class AsyncDictCursor(_async_cursor):
self.row_factory = dict_row self.row_factory = dict_row
return res return res
def fetchwindow(self, from_rownum=0, to_rownum=0, _tupples=False):
"""
Fetch many tuples as ordered dictionary list.
"""
self._odt_desc = None
self.row_factory = tuple_row
asyncio.run(self._scrollcur(from_rownum, "absolute"))
res = asyncio.run(self._fetchmany(to_rownum - from_rownum + 1))
if not _tupples and res is not None:
res = [self._dict_tuple(t) for t in res]
self.row_factory = dict_row
return res
async def _scrollcur(self, position, mode): async def _scrollcur(self, position, mode):
""" """
Fetch all tuples as ordered dictionary list. Fetch all tuples as ordered dictionary list.

View File

@ -47,10 +47,10 @@ class QueryToolFeatureTest(BaseFeatureTest):
def runTest(self): def runTest(self):
self._reset_options() self._reset_options()
# on demand result set on scrolling. # pagination result on page change.
print("\nOn demand query result... ", print("\nPagination query result... ",
file=sys.stderr, end="") file=sys.stderr, end="")
self._on_demand_result() self._pagination_result()
self.page.clear_query_tool() self.page.clear_query_tool()
# explain query with verbose and cost # explain query with verbose and cost
@ -129,18 +129,12 @@ class QueryToolFeatureTest(BaseFeatureTest):
# close menu # close menu
query_op.click() query_op.click()
def _on_demand_result(self): def _pagination_result(self):
ON_DEMAND_CHUNKS = 2 query = """-- Pagination result
row_id_to_find = config.ON_DEMAND_RECORD_COUNT * ON_DEMAND_CHUNKS
query = """-- On demand query result on scroll
-- Grid select all
-- Column select all
SELECT generate_series(1, {}) as id1, 'dummy' as id2""".format( SELECT generate_series(1, {}) as id1, 'dummy' as id2""".format(
config.ON_DEMAND_RECORD_COUNT * ON_DEMAND_CHUNKS) config.DATA_RESULT_ROWS_PER_PAGE * 2.5)
print("\nOn demand result set on scrolling... ", print("\nPagination result... ", file=sys.stderr, end="")
file=sys.stderr, end="")
self.page.execute_query(query) self.page.execute_query(query)
# wait for header of the table to be visible # wait for header of the table to be visible
@ -151,94 +145,33 @@ SELECT generate_series(1, {}) as id1, 'dummy' as id2""".format(
(By.CSS_SELECTOR, (By.CSS_SELECTOR,
QueryToolLocators.query_output_cells))) QueryToolLocators.query_output_cells)))
self.page.find_by_css_selector( for i, page in enumerate([
QueryToolLocators.query_output_canvas_css) {'page_info': '1 to 1000', 'cell_rownum': '1'},
{'page_info': '1001 to 2000', 'cell_rownum': '1001'},
{'page_info': '2001 to 2500', 'cell_rownum': '2001'}
]):
page_info = self.page.find_by_css_selector(
QueryToolLocators.pagination_inputs +
f' span:nth-of-type(1)')
self._check_ondemand_result(row_id_to_find) self.assertEqual(page_info.text, f"Showing: {page['page_info']}")
print("OK.", file=sys.stderr)
print("On demand result set on grid select all... ", page_info = self.page.find_by_css_selector(
file=sys.stderr, end="") QueryToolLocators.pagination_inputs + ' span:nth-of-type(3)')
self.page.click_execute_query_button()
# wait for header of the table to be visible self.assertEqual(page_info.text, "of 3")
self.page.find_by_css_selector(
QueryToolLocators.query_output_canvas_css)
# wait for the rows in the table to be displayed cell_rownum = self.page.find_by_css_selector(
self.wait.until(EC.presence_of_element_located( QueryToolLocators.query_output_cells + ':nth-of-type(1)')
(By.CSS_SELECTOR,
QueryToolLocators.query_output_cells))
)
# Select all rows in a table self.assertEqual(cell_rownum.text, page['cell_rownum'])
multiple_check = True
while multiple_check:
try:
select_all = self.wait.until(EC.element_to_be_clickable(
(By.XPATH, QueryToolLocators.select_all_column)))
select_all.click()
multiple_check = False
except (StaleElementReferenceException,
ElementClickInterceptedException):
pass
self._check_ondemand_result(row_id_to_find) if i < 2:
print("OK.", file=sys.stderr)
print("On demand result set on column select all... ",
file=sys.stderr, end="")
self.page.click_execute_query_button()
self.page.wait_for_query_tool_loading_indicator_to_disappear()
# wait for header of the table to be visible
self.wait.until(EC.visibility_of_element_located(
(By.CSS_SELECTOR, QueryToolLocators.query_output_canvas_css)))
# wait for the rows in the table to be displayed
self.wait.until(EC.presence_of_element_located(
(By.CSS_SELECTOR,
QueryToolLocators.query_output_cells))
)
self.wait.until(EC.presence_of_element_located(
(By.CSS_SELECTOR, QueryToolLocators.query_output_canvas_css)))
self._check_ondemand_result(row_id_to_find)
print("OK.", file=sys.stderr)
def _check_ondemand_result(self, row_id_to_find):
# scroll to bottom to bring last row of next chunk in viewport.
scroll = 10
status = False
while scroll:
# click on first data column to select all column.
column_1 = \
self.page.find_by_css_selector( self.page.find_by_css_selector(
QueryToolLocators.output_column_header_css.format('id1')) QueryToolLocators.pagination_inputs +
column_1.click() ' button[aria-label="Next Page"]').click()
grid = self.page.find_by_css_selector('.rdg')
scrolling_height = grid.size['height']
self.driver.execute_script(
"document.querySelector('.rdg').scrollTop="
"document.querySelector('.rdg').scrollHeight"
)
# Table height takes some time to update, for which their is no
# particular way
time.sleep(2)
if grid.size['height'] == scrolling_height and \
self.page.check_if_element_exist_by_xpath(
QueryToolLocators.output_column_data_xpath.format(
row_id_to_find)):
status = True
break
else:
scroll -= 1
self.assertTrue( self.page.wait_for_query_tool_loading_indicator_to_disappear()
status, "Element is not loaded to the rows id: "
"{}".format(row_id_to_find))
def _query_tool_explain_with_verbose_and_cost(self): def _query_tool_explain_with_verbose_and_cost(self):
query = """-- Explain query with verbose and cost query = """-- Explain query with verbose and cost

View File

@ -172,8 +172,6 @@ CREATE TABLE public.nonintpkey
self._copy_paste_row(config_data_local) self._copy_paste_row(config_data_local)
self._update_row(config_data_local) self._update_row(config_data_local)
self.page.click_tab("Messages")
self._verify_messsages("")
self.page.click_tab("Data Output") self.page.click_tab("Data Output")
updated_row_data = { updated_row_data = {
i: config_data_local['update'][i] if i in config_data_local[ i: config_data_local['update'][i] if i in config_data_local[

View File

@ -232,6 +232,8 @@ class QueryToolLocators:
query_output_canvas_css = "#id-dataoutput .rdg" query_output_canvas_css = "#id-dataoutput .rdg"
pagination_inputs = "#id-dataoutput .PaginationInputs"
query_output_cells = ".rdg-cell[role='gridcell']" query_output_cells = ".rdg-cell[role='gridcell']"
sql_editor_message = "//div[@id='id-messages'][contains(string(), '{}')]" sql_editor_message = "//div[@id='id-messages'][contains(string(), '{}')]"